Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/1.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
davedevelopment committed Aug 11, 2020
2 parents 0961703 + d55b877 commit 81d6dc3
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 78 deletions.
1 change: 1 addition & 0 deletions .styleci.yml
Expand Up @@ -9,4 +9,5 @@ disabled:

finder:
not-name:
- Php80LanguageFeaturesTest.php
- SemiReservedWordsAsMethods.php
5 changes: 4 additions & 1 deletion composer.json
Expand Up @@ -36,7 +36,7 @@
"hamcrest/hamcrest-php": "^2.0.1"
},
"require-dev": {
"phpunit/phpunit": "^8.5 || ^9.0"
"phpunit/phpunit": "^8.5 || ^9.3"
},
"conflict": {
"phpunit/phpunit": "<8.0"
Expand All @@ -51,6 +51,9 @@
"test\\": "tests/"
}
},
"config": {
"preferred-install": "dist"
},
"extra": {
"branch-alias": {
"dev-master": "1.4.x-dev"
Expand Down
128 changes: 60 additions & 68 deletions library/Mockery/Container.php
Expand Up @@ -120,80 +120,72 @@ public function mock(...$args)
$builder->setConstantsMap(\Mockery::getConfiguration()->getConstantsMap());

while (count($args) > 0) {
$arg = current($args);
$arg = array_shift($args);
// check for multiple interfaces
if (is_string($arg) && strpos($arg, ',') && !strpos($arg, ']')) {
$interfaces = explode(',', str_replace(' ', '', $arg));
$builder->addTargets($interfaces);
array_shift($args);

continue;
} elseif (is_string($arg) && substr($arg, 0, 6) == 'alias:') {
$name = array_shift($args);
$name = str_replace('alias:', '', $name);
$builder->addTarget('stdClass');
$builder->setName($name);
continue;
} elseif (is_string($arg) && substr($arg, 0, 9) == 'overload:') {
$name = array_shift($args);
$name = str_replace('overload:', '', $name);
$builder->setInstanceMock(true);
$builder->addTarget('stdClass');
$builder->setName($name);
continue;
} elseif (is_string($arg) && substr($arg, strlen($arg)-1, 1) == ']') {
$parts = explode('[', $arg);
if (!class_exists($parts[0], true) && !interface_exists($parts[0], true)) {
throw new \Mockery\Exception('Can only create a partial mock from'
. ' an existing class or interface');
}
$class = $parts[0];
$parts[1] = str_replace(' ', '', $parts[1]);
$partialMethods = array_filter(explode(',', strtolower(rtrim($parts[1], ']'))));
$builder->addTarget($class);
foreach ($partialMethods as $partialMethod) {
if ($partialMethod[0] === '!') {
$builder->addBlackListedMethod(substr($partialMethod, 1));
continue;
if (is_string($arg)) {
foreach (explode('|', $arg) as $type) {
if ($arg === 'null') {
// skip PHP 8 'null's
} elseif (strpos($type, ',') && !strpos($type, ']')) {
$interfaces = explode(',', str_replace(' ', '', $type));
$builder->addTargets($interfaces);
} elseif (substr($type, 0, 6) == 'alias:') {
$type = str_replace('alias:', '', $type);
$builder->addTarget('stdClass');
$builder->setName($type);
} elseif (substr($type, 0, 9) == 'overload:') {
$type = str_replace('overload:', '', $type);
$builder->setInstanceMock(true);
$builder->addTarget('stdClass');
$builder->setName($type);
} elseif (substr($type, strlen($type)-1, 1) == ']') {
$parts = explode('[', $type);
if (!class_exists($parts[0], true) && !interface_exists($parts[0], true)) {
throw new \Mockery\Exception('Can only create a partial mock from'
. ' an existing class or interface');
}
$class = $parts[0];
$parts[1] = str_replace(' ', '', $parts[1]);
$partialMethods = array_filter(explode(',', strtolower(rtrim($parts[1], ']'))));
$builder->addTarget($class);
foreach ($partialMethods as $partialMethod) {
if ($partialMethod[0] === '!') {
$builder->addBlackListedMethod(substr($partialMethod, 1));
continue;
}
$builder->addWhiteListedMethod($partialMethod);
}
} elseif (class_exists($type, true) || interface_exists($type, true) || trait_exists($type, true)) {
$builder->addTarget($type);
} elseif (!\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed() && (!class_exists($type, true) && !interface_exists($type, true))) {
throw new \Mockery\Exception("Mockery can't find '$type' so can't mock it");
} else {
if (!$this->isValidClassName($type)) {
throw new \Mockery\Exception('Class name contains invalid characters');
}
$builder->addTarget($type);
}
$builder->addWhiteListedMethod($partialMethod);
}
array_shift($args);
continue;
} elseif (is_string($arg) && (class_exists($arg, true) || interface_exists($arg, true) || trait_exists($arg, true))) {
$class = array_shift($args);
$builder->addTarget($class);
continue;
} elseif (is_string($arg) && !\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed() && (!class_exists($arg, true) && !interface_exists($arg, true))) {
throw new \Mockery\Exception("Mockery can't find '$arg' so can't mock it");
} elseif (is_string($arg)) {
if (!$this->isValidClassName($arg)) {
throw new \Mockery\Exception('Class name contains invalid characters');
break; // unions are "sum" types and not "intersections", and so we must only process the first part
}
$class = array_shift($args);
$builder->addTarget($class);
continue;
} elseif (is_object($arg)) {
$partial = array_shift($args);
$builder->addTarget($partial);
continue;
} elseif (is_array($arg) && !empty($arg) && array_keys($arg) !== range(0, count($arg) - 1)) {
// if associative array
if (array_key_exists(self::BLOCKS, $arg)) {
$blocks = $arg[self::BLOCKS];
}
unset($arg[self::BLOCKS]);
$quickdefs = array_shift($args);
continue;
$builder->addTarget($arg);
} elseif (is_array($arg)) {
$constructorArgs = array_shift($args);
continue;
if (!empty($arg) && array_keys($arg) !== range(0, count($arg) - 1)) {
// if associative array
if (array_key_exists(self::BLOCKS, $arg)) {
$blocks = $arg[self::BLOCKS];
}
unset($arg[self::BLOCKS]);
$quickdefs = $arg;
} else {
$constructorArgs = $arg;
}
} else {
throw new \Mockery\Exception(
'Unable to parse arguments sent to '
. get_class($this) . '::mock()'
);
}

throw new \Mockery\Exception(
'Unable to parse arguments sent to '
. get_class($this) . '::mock()'
);
}

$builder->addBlackListedMethods($blocks);
Expand Down
2 changes: 2 additions & 0 deletions library/Mockery/Generator/Parameter.php
Expand Up @@ -46,6 +46,8 @@ public function __call($method, array $args)
* This will be null if there was no type, or it was a scalar or a union.
*
* @return \ReflectionClass|null
*
* @deprecated since 1.3.3 and will be removed in 2.0.
*/
public function getClass()
{
Expand Down
130 changes: 122 additions & 8 deletions library/Mockery/Reflector.php
Expand Up @@ -55,10 +55,10 @@ public static function getTypeHint(\ReflectionParameter $param, $withoutNullable
}

$type = $param->getType();
$declaringClass = $param->getDeclaringClass()->getName();
$declaringClass = $param->getDeclaringClass();
$typeHint = self::typeToString($type, $declaringClass);

return (!$withoutNullable && $type->allowsNull()) ? sprintf('?%s', $typeHint) : $typeHint;
return (!$withoutNullable && $type->allowsNull()) ? self::formatNullableType($typeHint) : $typeHint;
}

/**
Expand All @@ -76,10 +76,92 @@ public static function getReturnType(\ReflectionMethod $method, $withoutNullable
}

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

return (!$withoutNullable && $type->allowsNull()) ? sprintf('?%s', $typeHint) : $typeHint;
return (!$withoutNullable && $type->allowsNull()) ? self::formatNullableType($typeHint) : $typeHint;
}

/**
* Compute the legacy type hint.
*
* We return:
* - string: the legacy type hint
* - null: if there is no legacy type hint
* - false: if we must check for PHP 7+ typing
*
* @param \ReflectionParameter $param
*
* @return string|null|false
*/
private static function getLegacyTypeHint(\ReflectionParameter $param)
{
// Handle HHVM typing
if (\method_exists($param, 'getTypehintText')) {
if ($param->isArray()) {
return 'array';
}

if ($param->isCallable()) {
return 'callable';
}

$typeHint = $param->getTypehintText();

// throw away HHVM scalar types
if (\in_array($typeHint, array('int', 'integer', 'float', 'string', 'bool', 'boolean'), true)) {
return null;
}

return sprintf('\\%s', $typeHint);
}

// Handle PHP 5 typing
if (\PHP_VERSION_ID < 70000) {
if ($param->isArray()) {
return 'array';
}

if ($param->isCallable()) {
return 'callable';
}

$typeHint = self::getLegacyClassName($param);

return $typeHint === null ? null : sprintf('\\%s', $typeHint);
}

return false;
}

/**
* Compute the class name using legacy APIs, if possible.
*
* This method MUST only be called on PHP 5.
*
* @param \ReflectionParameter $param
*
* @return string|null
*/
private static function getLegacyClassName(\ReflectionParameter $param)
{
try {
$class = $param->getClass();

$typeHint = $class === null ? null : $class->getName();
} catch (\ReflectionException $e) {
$typeHint = null;
}

if ($typeHint === null) {
if (preg_match('/^Parameter #[0-9]+ \[ \<(required|optional)\> (?<typehint>\S+ )?.*\$' . $param->getName() . ' .*\]$/', (string) $param, $typehintMatch)) {
if (!empty($typehintMatch['typehint']) && $typehintMatch['typehint']) {
$typeHint = $typehintMatch['typehint'];
}
}
}

return $typeHint;
}

/**
Expand All @@ -90,7 +172,7 @@ public static function getReturnType(\ReflectionMethod $method, $withoutNullable
*
* @return string|null
*/
private static function typeToString(\ReflectionType $type, $declaringClass)
private static function typeToString(\ReflectionType $type, \ReflectionClass $declaringClass)
{
// PHP 8 union types can be recursively processed
if ($type instanceof \ReflectionUnionType) {
Expand All @@ -102,8 +184,40 @@ private static function typeToString(\ReflectionType $type, $declaringClass)
// $type must be an instance of \ReflectionNamedType
$typeHint = $type->getName();

// 'self' needs to be resolved to the name of the declaring class and
// 'static' is a special type reserved as a return type in PHP 8
return ($type->isBuiltin() || $typeHint === 'static') ? $typeHint : sprintf('\\%s', $typeHint === 'self' ? $declaringClass : $typeHint);
// builtins and 'static' can be returned as is
if (($type->isBuiltin() || $typeHint === 'static')) {
return $typeHint;
}

// 'self' needs to be resolved to the name of the declaring class
if ($typeHint === 'self') {
$typeHint = $declaringClass->getName();
}

// 'parent' needs to be resolved to the name of the parent class
if ($typeHint === 'parent') {
$typeHint = $declaringClass->getParentClass()->getName();
}

// class names need prefixing with a slash
return sprintf('\\%s', $typeHint);
}

/**
* Format the given type as a nullable type.
*
* This method MUST only be called on PHP 7.1+.
*
* @param string $typeHint
*
* @return string
*/
private static function formatNullableType($typeHint)
{
if (\PHP_VERSION_ID < 80000) {
return sprintf('?%s', $typeHint);
}

return $typeHint === 'mixed' ? 'mixed' : sprintf('%s|null', $typeHint);
}
}
7 changes: 6 additions & 1 deletion phpunit.xml.dist
Expand Up @@ -4,7 +4,12 @@
verbose="true"
>
<testsuites>
<testsuite name="Mockery Test Suite">
<testsuite name="Mockery Test Suite PHP8">
<directory suffix="Test.php">./tests</directory>
<directory phpVersion="8.0.0" phpVersionOperator=">=">./tests/PHP80</directory>
</testsuite>

<testsuite name="Mockery Test Suite PHP7">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
Expand Down

1 comment on commit 81d6dc3

@GrahamCampbell
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That merge seems to have not gone quite right. I'll send a PR to fix up.

Please sign in to comment.