You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
386 lines
11 KiB
386 lines
11 KiB
<?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\Generator;
|
|
|
|
use function count;
|
|
use function explode;
|
|
use function implode;
|
|
use function is_object;
|
|
use function is_string;
|
|
use function preg_match;
|
|
use function preg_replace;
|
|
use function sprintf;
|
|
use function strlen;
|
|
use function strpos;
|
|
use function substr;
|
|
use function substr_count;
|
|
use function trim;
|
|
use function var_export;
|
|
use ReflectionMethod;
|
|
use ReflectionParameter;
|
|
use SebastianBergmann\Type\ReflectionMapper;
|
|
use SebastianBergmann\Type\Type;
|
|
use SebastianBergmann\Type\UnknownType;
|
|
|
|
/**
|
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
|
*/
|
|
final class MockMethod
|
|
{
|
|
use TemplateLoader;
|
|
|
|
/**
|
|
* @psalm-var class-string
|
|
*/
|
|
private readonly string $className;
|
|
|
|
/**
|
|
* @psalm-var non-empty-string
|
|
*/
|
|
private readonly string $methodName;
|
|
private readonly bool $cloneArguments;
|
|
private readonly string $modifier;
|
|
private readonly string $argumentsForDeclaration;
|
|
private readonly string $argumentsForCall;
|
|
private readonly Type $returnType;
|
|
private readonly string $reference;
|
|
private readonly bool $callOriginalMethod;
|
|
private readonly bool $static;
|
|
private readonly ?string $deprecation;
|
|
|
|
/**
|
|
* @psalm-var array<int, mixed>
|
|
*/
|
|
private readonly array $defaultParameterValues;
|
|
|
|
/**
|
|
* @psalm-var non-negative-int
|
|
*/
|
|
private readonly int $numberOfParameters;
|
|
|
|
/**
|
|
* @throws ReflectionException
|
|
* @throws RuntimeException
|
|
*/
|
|
public static function fromReflection(ReflectionMethod $method, bool $callOriginalMethod, bool $cloneArguments): self
|
|
{
|
|
if ($method->isPrivate()) {
|
|
$modifier = 'private';
|
|
} elseif ($method->isProtected()) {
|
|
$modifier = 'protected';
|
|
} else {
|
|
$modifier = 'public';
|
|
}
|
|
|
|
if ($method->isStatic()) {
|
|
$modifier .= ' static';
|
|
}
|
|
|
|
if ($method->returnsReference()) {
|
|
$reference = '&';
|
|
} else {
|
|
$reference = '';
|
|
}
|
|
|
|
$docComment = $method->getDocComment();
|
|
|
|
if (is_string($docComment) &&
|
|
preg_match('#\*[ \t]*+@deprecated[ \t]*+(.*?)\r?+\n[ \t]*+\*(?:[ \t]*+@|/$)#s', $docComment, $deprecation)) {
|
|
$deprecation = trim(preg_replace('#[ \t]*\r?\n[ \t]*+\*[ \t]*+#', ' ', $deprecation[1]));
|
|
} else {
|
|
$deprecation = null;
|
|
}
|
|
|
|
return new self(
|
|
$method->getDeclaringClass()->getName(),
|
|
$method->getName(),
|
|
$cloneArguments,
|
|
$modifier,
|
|
self::methodParametersForDeclaration($method),
|
|
self::methodParametersForCall($method),
|
|
self::methodParametersDefaultValues($method),
|
|
count($method->getParameters()),
|
|
(new ReflectionMapper)->fromReturnType($method),
|
|
$reference,
|
|
$callOriginalMethod,
|
|
$method->isStatic(),
|
|
$deprecation,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param class-string $className
|
|
* @param non-empty-string $methodName
|
|
*/
|
|
public static function fromName(string $className, string $methodName, bool $cloneArguments): self
|
|
{
|
|
return new self(
|
|
$className,
|
|
$methodName,
|
|
$cloneArguments,
|
|
'public',
|
|
'',
|
|
'',
|
|
[],
|
|
0,
|
|
new UnknownType,
|
|
'',
|
|
false,
|
|
false,
|
|
null,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @psalm-param class-string $className
|
|
* @psalm-param non-empty-string $methodName
|
|
* @psalm-param array<int, mixed> $defaultParameterValues
|
|
* @psalm-param non-negative-int $numberOfParameters
|
|
*/
|
|
private function __construct(string $className, string $methodName, bool $cloneArguments, string $modifier, string $argumentsForDeclaration, string $argumentsForCall, array $defaultParameterValues, int $numberOfParameters, Type $returnType, string $reference, bool $callOriginalMethod, bool $static, ?string $deprecation)
|
|
{
|
|
$this->className = $className;
|
|
$this->methodName = $methodName;
|
|
$this->cloneArguments = $cloneArguments;
|
|
$this->modifier = $modifier;
|
|
$this->argumentsForDeclaration = $argumentsForDeclaration;
|
|
$this->argumentsForCall = $argumentsForCall;
|
|
$this->defaultParameterValues = $defaultParameterValues;
|
|
$this->numberOfParameters = $numberOfParameters;
|
|
$this->returnType = $returnType;
|
|
$this->reference = $reference;
|
|
$this->callOriginalMethod = $callOriginalMethod;
|
|
$this->static = $static;
|
|
$this->deprecation = $deprecation;
|
|
}
|
|
|
|
/**
|
|
* @psalm-return non-empty-string
|
|
*/
|
|
public function methodName(): string
|
|
{
|
|
return $this->methodName;
|
|
}
|
|
|
|
/**
|
|
* @throws RuntimeException
|
|
*/
|
|
public function generateCode(): string
|
|
{
|
|
if ($this->static) {
|
|
$templateFile = 'doubled_static_method.tpl';
|
|
} else {
|
|
$templateFile = sprintf(
|
|
'%s_method.tpl',
|
|
$this->callOriginalMethod ? 'proxied' : 'doubled',
|
|
);
|
|
}
|
|
|
|
$deprecation = $this->deprecation;
|
|
$returnResult = '';
|
|
|
|
if (!$this->returnType->isNever() && !$this->returnType->isVoid()) {
|
|
$returnResult = <<<'EOT'
|
|
|
|
|
|
return $__phpunit_result;
|
|
EOT;
|
|
}
|
|
|
|
if (null !== $this->deprecation) {
|
|
$deprecation = "The {$this->className}::{$this->methodName} method is deprecated ({$this->deprecation}).";
|
|
$deprecationTemplate = $this->loadTemplate('deprecation.tpl');
|
|
|
|
$deprecationTemplate->setVar(
|
|
[
|
|
'deprecation' => var_export($deprecation, true),
|
|
],
|
|
);
|
|
|
|
$deprecation = $deprecationTemplate->render();
|
|
}
|
|
|
|
$template = $this->loadTemplate($templateFile);
|
|
|
|
$template->setVar(
|
|
[
|
|
'arguments_decl' => $this->argumentsForDeclaration,
|
|
'arguments_call' => $this->argumentsForCall,
|
|
'return_declaration' => !empty($this->returnType->asString()) ? (': ' . $this->returnType->asString()) : '',
|
|
'return_type' => $this->returnType->asString(),
|
|
'arguments_count' => !empty($this->argumentsForCall) ? substr_count($this->argumentsForCall, ',') + 1 : 0,
|
|
'class_name' => $this->className,
|
|
'method_name' => $this->methodName,
|
|
'modifier' => $this->modifier,
|
|
'reference' => $this->reference,
|
|
'clone_arguments' => $this->cloneArguments ? 'true' : 'false',
|
|
'deprecation' => $deprecation,
|
|
'return_result' => $returnResult,
|
|
],
|
|
);
|
|
|
|
return $template->render();
|
|
}
|
|
|
|
public function returnType(): Type
|
|
{
|
|
return $this->returnType;
|
|
}
|
|
|
|
/**
|
|
* @psalm-return array<int, mixed>
|
|
*/
|
|
public function defaultParameterValues(): array
|
|
{
|
|
return $this->defaultParameterValues;
|
|
}
|
|
|
|
/**
|
|
* @psalm-return non-negative-int
|
|
*/
|
|
public function numberOfParameters(): int
|
|
{
|
|
return $this->numberOfParameters;
|
|
}
|
|
|
|
/**
|
|
* Returns the parameters of a function or method.
|
|
*
|
|
* @throws RuntimeException
|
|
*/
|
|
private static function methodParametersForDeclaration(ReflectionMethod $method): string
|
|
{
|
|
$parameters = [];
|
|
$types = (new ReflectionMapper)->fromParameterTypes($method);
|
|
|
|
foreach ($method->getParameters() as $i => $parameter) {
|
|
$name = '$' . $parameter->getName();
|
|
|
|
/* Note: PHP extensions may use empty names for reference arguments
|
|
* or "..." for methods taking a variable number of arguments.
|
|
*/
|
|
if ($name === '$' || $name === '$...') {
|
|
$name = '$arg' . $i;
|
|
}
|
|
|
|
$default = '';
|
|
$reference = '';
|
|
$typeDeclaration = '';
|
|
|
|
if (!$types[$i]->type()->isUnknown()) {
|
|
$typeDeclaration = $types[$i]->type()->asString() . ' ';
|
|
}
|
|
|
|
if ($parameter->isPassedByReference()) {
|
|
$reference = '&';
|
|
}
|
|
|
|
if ($parameter->isVariadic()) {
|
|
$name = '...' . $name;
|
|
} elseif ($parameter->isDefaultValueAvailable()) {
|
|
$default = ' = ' . self::exportDefaultValue($parameter);
|
|
} elseif ($parameter->isOptional()) {
|
|
$default = ' = null';
|
|
}
|
|
|
|
$parameters[] = $typeDeclaration . $reference . $name . $default;
|
|
}
|
|
|
|
return implode(', ', $parameters);
|
|
}
|
|
|
|
/**
|
|
* Returns the parameters of a function or method.
|
|
*
|
|
* @throws ReflectionException
|
|
*/
|
|
private static function methodParametersForCall(ReflectionMethod $method): string
|
|
{
|
|
$parameters = [];
|
|
|
|
foreach ($method->getParameters() as $i => $parameter) {
|
|
$name = '$' . $parameter->getName();
|
|
|
|
/* Note: PHP extensions may use empty names for reference arguments
|
|
* or "..." for methods taking a variable number of arguments.
|
|
*/
|
|
if ($name === '$' || $name === '$...') {
|
|
$name = '$arg' . $i;
|
|
}
|
|
|
|
if ($parameter->isVariadic()) {
|
|
continue;
|
|
}
|
|
|
|
if ($parameter->isPassedByReference()) {
|
|
$parameters[] = '&' . $name;
|
|
} else {
|
|
$parameters[] = $name;
|
|
}
|
|
}
|
|
|
|
return implode(', ', $parameters);
|
|
}
|
|
|
|
/**
|
|
* @throws ReflectionException
|
|
*/
|
|
private static function exportDefaultValue(ReflectionParameter $parameter): string
|
|
{
|
|
try {
|
|
$defaultValue = $parameter->getDefaultValue();
|
|
|
|
if (!is_object($defaultValue)) {
|
|
return var_export($defaultValue, true);
|
|
}
|
|
|
|
$parameterAsString = $parameter->__toString();
|
|
|
|
return explode(
|
|
' = ',
|
|
substr(
|
|
substr(
|
|
$parameterAsString,
|
|
strpos($parameterAsString, '<optional> ') + strlen('<optional> '),
|
|
),
|
|
0,
|
|
-2,
|
|
),
|
|
)[1];
|
|
// @codeCoverageIgnoreStart
|
|
} catch (\ReflectionException $e) {
|
|
throw new ReflectionException(
|
|
$e->getMessage(),
|
|
$e->getCode(),
|
|
$e,
|
|
);
|
|
}
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
/**
|
|
* @psalm-return array<int, mixed>
|
|
*/
|
|
private static function methodParametersDefaultValues(ReflectionMethod $method): array
|
|
{
|
|
$result = [];
|
|
|
|
foreach ($method->getParameters() as $i => $parameter) {
|
|
if (!$parameter->isDefaultValueAvailable()) {
|
|
continue;
|
|
}
|
|
|
|
$result[$i] = $parameter->getDefaultValue();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|