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

Only allow valid return types for willReturn #3673

Merged
merged 11 commits into from May 19, 2019
57 changes: 51 additions & 6 deletions src/Framework/MockObject/Builder/InvocationMocker.php
Expand Up @@ -10,6 +10,8 @@
namespace PHPUnit\Framework\MockObject\Builder;

use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\MockObject\ConfigurableMethod;
use PHPUnit\Framework\MockObject\IncompatibleReturnValueException;
use PHPUnit\Framework\MockObject\Matcher;
use PHPUnit\Framework\MockObject\Matcher\Invocation;
use PHPUnit\Framework\MockObject\RuntimeException;
Expand All @@ -32,11 +34,11 @@ final class InvocationMocker implements MethodNameMatch
private $matcher;

/**
* @var string[]
* @var ConfigurableMethod[]
*/
private $configurableMethods;

public function __construct(MatcherCollection $collection, Invocation $invocationMatcher, array $configurableMethods)
public function __construct(MatcherCollection $collection, Invocation $invocationMatcher, ConfigurableMethod...$configurableMethods)
{
$this->collection = $collection;
$this->matcher = new Matcher($invocationMatcher);
Expand Down Expand Up @@ -68,11 +70,12 @@ public function will(Stub $stub): Identity
public function willReturn($value, ...$nextValues): self
{
if (\count($nextValues) === 0) {
$this->ensureTypeOfReturnValues([$value]);
$stub = new Stub\ReturnStub($value);
} else {
$stub = new Stub\ConsecutiveCalls(
\array_merge([$value], $nextValues)
);
$values = \array_merge([$value], $nextValues);
$this->ensureTypeOfReturnValues($values);
$stub = new Stub\ConsecutiveCalls($values);
}

return $this->will($stub);
Expand Down Expand Up @@ -193,7 +196,14 @@ public function method($constraint): self
);
}

if (\is_string($constraint) && !\in_array(\strtolower($constraint), $this->configurableMethods, true)) {
$configurableMethodNames = \array_map(
function (ConfigurableMethod $configurable) {
return \strtolower($configurable->getName());
},
$this->configurableMethods
);

if (\is_string($constraint) && !\in_array(\strtolower($constraint), $configurableMethodNames, true)) {
throw new RuntimeException(
\sprintf(
'Trying to configure method "%s" which cannot be configured because it does not exist, has not been specified, is final, or is static',
Expand Down Expand Up @@ -227,4 +237,39 @@ private function canDefineParameters(): void
);
}
}

private function getConfiguredMethod(): ?ConfigurableMethod
{
$configuredMethod = null;

foreach ($this->configurableMethods as $configurableMethod) {
if ($this->matcher->getMethodNameMatcher()->matchesName($configurableMethod->getName())) {
if ($configuredMethod !== null) {
return null;
}
$configuredMethod = $configurableMethod;
}
}

return $configuredMethod;
}

private function ensureTypeOfReturnValues(array $values): void
{
$configuredMethod = $this->getConfiguredMethod();

if ($configuredMethod === null) {
return;
}

foreach ($values as $value) {
if (!$configuredMethod->mayReturn($value)) {
throw new IncompatibleReturnValueException(\sprintf(
'Method %s may not return value of type %s',
$configuredMethod->getName(),
\gettype($value)
));
}
}
}
}
46 changes: 46 additions & 0 deletions src/Framework/MockObject/ConfigurableMethod.php
@@ -0,0 +1,46 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Framework\MockObject;

/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class ConfigurableMethod
{
/**
* @var string
*/
private $name;

/**
* @var Type
*/
private $returnType;

public function __construct(string $name, Type $returnType)
{
$this->name = $name;
$this->returnType = $returnType;
}

public function getName(): string
{
return $this->name;
}

public function mayReturn($value): bool
{
if ($value === null && $this->returnType->allowsNull()) {
return true;
}

return $this->returnType->isAssignable(Type::fromValue($value, false));
}
}
29 changes: 29 additions & 0 deletions src/Framework/MockObject/ConfigurableMethods.php
@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Framework\MockObject;

/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
trait ConfigurableMethods
{
/**
* @var ConfigurableMethods[]
*/
private static $__phpunit_configurableMethods;

public static function __phpunit_initConfigurableMethods(\PHPUnit\Framework\MockObject\ConfigurableMethod...$configurable): void
{
if (isset(static::$__phpunit_configurableMethods)) {
throw new ConfigurableMethodsAlreadyInitializedException('Configurable methods is already initialized and can not be reinitialized.');
}
static::$__phpunit_configurableMethods = $configurable;
}
}
@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Framework\MockObject;

/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class ConfigurableMethodsAlreadyInitializedException extends \PHPUnit\Framework\Exception implements Exception
{
}
@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Framework\MockObject;

/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class IncompatibleReturnValueException extends \PHPUnit\Framework\Exception implements Exception
{
}
57 changes: 14 additions & 43 deletions src/Framework/MockObject/Generator.php
Expand Up @@ -167,8 +167,7 @@ function ($type) {
);

return $this->getObject(
$mock['code'],
$mock['mockClassName'],
$mock,
$type,
$callOriginalConstructor,
$callAutoload,
Expand Down Expand Up @@ -263,10 +262,8 @@ public function getMockForTrait(string $traitName, array $arguments = [], string
]
);

$this->evalClass(
$classTemplate->render(),
$className['className']
);
$mockTrait = new MockTrait($classTemplate->render(), $className['className']);
$mockTrait->generate();

return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments);
}
Expand Down Expand Up @@ -303,15 +300,10 @@ public function getObjectForTrait(string $traitName, string $traitClassName = ''
]
);

return $this->getObject($classTemplate->render(), $className['className']);
return $this->getObject(new MockTrait($classTemplate->render(), $className['className']));
}

/**
* @param array|string $type
*
* @throws RuntimeException
*/
public function generate($type, array $methods = null, string $mockClassName = '', bool $callOriginalClone = true, bool $callAutoload = true, bool $cloneArguments = true, bool $callOriginalMethods = false): array
public function generate($type, array $methods = null, string $mockClassName = '', bool $callOriginalClone = true, bool $callAutoload = true, bool $cloneArguments = true, bool $callOriginalMethods = false): MockClass
{
if (\is_array($type)) {
\sort($type);
Expand Down Expand Up @@ -522,14 +514,9 @@ private function getInterfaceOwnMethods(string $interfaceName): array
return $methods;
}

/**
* @param array|string $type
*
* @throws RuntimeException
*/
private function getObject(string $code, string $className, $type = '', bool $callOriginalConstructor = false, bool $callAutoload = false, array $arguments = [], bool $callOriginalMethods = false, object $proxyTarget = null, bool $returnValueGeneration = true)
private function getObject(MockType $mockClass, $type = '', bool $callOriginalConstructor = false, bool $callAutoload = false, array $arguments = [], bool $callOriginalMethods = false, object $proxyTarget = null, bool $returnValueGeneration = true)
{
$this->evalClass($code, $className);
$className = $mockClass->generate();

if ($callOriginalConstructor &&
\is_string($type) &&
Expand Down Expand Up @@ -586,20 +573,12 @@ private function getObject(string $code, string $className, $type = '', bool $ca
return $object;
}

private function evalClass(string $code, string $className): void
{
if (!\class_exists($className, false)) {
eval($code);
}
}

/**
* @param array|string $type
* @param null|array $explicitMethods
*
* @throws RuntimeException
*/
private function generateMock($type, $explicitMethods, string $mockClassName, bool $callOriginalClone, bool $callAutoload, bool $cloneArguments, bool $callOriginalMethods): array
private function generateMock($type, ?array $explicitMethods, string $mockClassName, bool $callOriginalClone, bool $callAutoload, bool $cloneArguments, bool $callOriginalMethods): MockClass
{
$classTemplate = $this->getTemplate('mocked_class.tpl');

Expand Down Expand Up @@ -821,7 +800,7 @@ private function generateMock($type, $explicitMethods, string $mockClassName, bo

foreach ($mockMethods->asArray() as $mockMethod) {
$mockedMethods .= $mockMethod->generateCode();
$configurable[] = \strtolower($mockMethod->getName());
$configurable[] = new ConfigurableMethod($mockMethod->getName(), $mockMethod->getReturnType());
}

$method = '';
Expand All @@ -843,22 +822,14 @@ private function generateMock($type, $explicitMethods, string $mockClassName, bo
'mock_class_name' => $mockClassName['className'],
'mocked_methods' => $mockedMethods,
'method' => $method,
'configurable' => '[' . \implode(
', ',
\array_map(
function ($m) {
return '\'' . $m . '\'';
},
$configurable
)
) . ']',
]
);

return [
'code' => $classTemplate->render(),
'mockClassName' => $mockClassName['className'],
];
return new MockClass(
$classTemplate->render(),
$mockClassName['className'],
$configurable
);
}

/**
Expand Down
7 changes: 5 additions & 2 deletions src/Framework/MockObject/Generator/mocked_class.tpl.dist
@@ -1,8 +1,11 @@
declare(strict_types=1);

{prologue}{class_declaration}
{
use \PHPUnit\Framework\MockObject\ConfigurableMethods;

private $__phpunit_invocationMocker;
private $__phpunit_originalObject;
private $__phpunit_configurable = {configurable};
private $__phpunit_returnValueGeneration = true;

{clone}{mocked_methods}
Expand All @@ -24,7 +27,7 @@
public function __phpunit_getInvocationMocker(): \PHPUnit\Framework\MockObject\InvocationMocker
{
if ($this->__phpunit_invocationMocker === null) {
$this->__phpunit_invocationMocker = new \PHPUnit\Framework\MockObject\InvocationMocker($this->__phpunit_configurable, $this->__phpunit_returnValueGeneration);
$this->__phpunit_invocationMocker = new \PHPUnit\Framework\MockObject\InvocationMocker(static::$__phpunit_configurableMethods, $this->__phpunit_returnValueGeneration);
}

return $this->__phpunit_invocationMocker;
Expand Down
4 changes: 2 additions & 2 deletions src/Framework/MockObject/Generator/mocked_method.tpl.dist
@@ -1,5 +1,5 @@

{modifier} function {reference}{method_name}({arguments_decl}){return_delim}{return_type}
{modifier} function {reference}{method_name}({arguments_decl}){return_declaration}
{{deprecation}
$__phpunit_arguments = [{arguments_call}];
$__phpunit_count = func_num_args();
Expand All @@ -14,7 +14,7 @@

$__phpunit_result = $this->__phpunit_getInvocationMocker()->invoke(
new \PHPUnit\Framework\MockObject\Invocation(
'{class_name}', '{method_name}', $__phpunit_arguments, '{return_type}', $this, {clone_arguments}
'{class_name}', '{method_name}', $__phpunit_arguments, '{return_declaration}', $this, {clone_arguments}
)
);

Expand Down
@@ -1,5 +1,5 @@

{modifier} function {reference}{method_name}({arguments_decl}){return_delim}{return_type}
{modifier} function {reference}{method_name}({arguments_decl}){return_declaration}
{{deprecation}
$__phpunit_arguments = [{arguments_call}];
$__phpunit_count = func_num_args();
Expand All @@ -14,7 +14,7 @@

$this->__phpunit_getInvocationMocker()->invoke(
new \PHPUnit\Framework\MockObject\Invocation(
'{class_name}', '{method_name}', $__phpunit_arguments, '{return_type}', $this, {clone_arguments}
'{class_name}', '{method_name}', $__phpunit_arguments, '{return_declaration}', $this, {clone_arguments}
)
);
}
@@ -1,5 +1,5 @@

{modifier} function {reference}{method_name}({arguments_decl}){return_delim}{return_type}
{modifier} function {reference}{method_name}({arguments_decl}){return_declaration}
{
throw new \PHPUnit\Framework\MockObject\BadMethodCallException('Static method "{method_name}" cannot be invoked on mock object');
}