diff --git a/.styleci.yml b/.styleci.yml index 91d210bed..a1a1caa0c 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -9,4 +9,5 @@ disabled: finder: not-name: + - Php80LanguageFeaturesTest.php - SemiReservedWordsAsMethods.php diff --git a/.travis.yml b/.travis.yml index d8e69cd60..18bdb88de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,7 +58,7 @@ before_install: ' >> ~/.phpenv/versions/"$(phpenv version-name)"/etc/conf.d/travis.ini fi if [[ $TRAVIS_PHP_VERSION == "nightly" ]]; then - composer require --dev --no-update "phpunit/phpunit:^8.5|^9.0" + composer require --dev --no-update "phpunit/phpunit:^9.3.2" fi install: @@ -68,9 +68,11 @@ install: script: - | if [[ $TRAVIS_PHP_VERSION == 5.6 ]]; then - ./vendor/bin/phpunit --coverage-text --coverage-clover="build/logs/clover.xml" --testsuite="Mockery Test Suite PHP56"; + ./vendor/bin/phpunit --coverage-text --coverage-clover="build/logs/clover.xml" --testsuite="Mockery Test Suite PHP5"; + elif [[ $TRAVIS_PHP_VERSION == 'nightly' ]]; then + ./vendor/bin/phpunit --coverage-text --coverage-clover="build/logs/clover.xml" --testsuite="Mockery Test Suite PHP8"; else - ./vendor/bin/phpunit --coverage-text --coverage-clover="build/logs/clover.xml" --testsuite="Mockery Test Suite"; + ./vendor/bin/phpunit --coverage-text --coverage-clover="build/logs/clover.xml" --testsuite="Mockery Test Suite PHP7"; fi after_success: diff --git a/composer.json b/composer.json index 42a077be2..44ffe5131 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ "hamcrest/hamcrest-php": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0|~9.0" + "phpunit/phpunit": "^5.7.10|^6.5|^7.5|^8.5|^9.3" }, "autoload": { "psr-0": { @@ -48,6 +48,9 @@ "test\\": "tests/" } }, + "config": { + "preferred-install": "dist" + }, "extra": { "branch-alias": { "dev-master": "1.3.x-dev" diff --git a/library/Mockery/Container.php b/library/Mockery/Container.php index e50a15611..2e719daf3 100644 --- a/library/Mockery/Container.php +++ b/library/Mockery/Container.php @@ -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); diff --git a/library/Mockery/Generator/Parameter.php b/library/Mockery/Generator/Parameter.php index d16787b52..700953369 100644 --- a/library/Mockery/Generator/Parameter.php +++ b/library/Mockery/Generator/Parameter.php @@ -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() { diff --git a/library/Mockery/Reflector.php b/library/Mockery/Reflector.php index 51db155ac..524d20cd2 100644 --- a/library/Mockery/Reflector.php +++ b/library/Mockery/Reflector.php @@ -66,11 +66,11 @@ public static function getTypeHint(\ReflectionParameter $param, $withoutNullable } $type = $param->getType(); - $declaringClass = $param->getDeclaringClass()->getName(); + $declaringClass = $param->getDeclaringClass(); $typeHint = self::typeToString($type, $declaringClass); // PHP 7.1+ supports nullable types via a leading question mark - return (!$withoutNullable && \PHP_VERSION_ID >= 70100 && $type->allowsNull()) ? sprintf('?%s', $typeHint) : $typeHint; + return (!$withoutNullable && \PHP_VERSION_ID >= 70100 && $type->allowsNull()) ? self::formatNullableType($typeHint) : $typeHint; } /** @@ -89,11 +89,11 @@ public static function getReturnType(\ReflectionMethod $method, $withoutNullable } $type = $method->getReturnType(); - $declaringClass = $method->getDeclaringClass()->getName(); + $declaringClass = $method->getDeclaringClass(); $typeHint = self::typeToString($type, $declaringClass); // PHP 7.1+ supports nullable types via a leading question mark - return (!$withoutNullable && \PHP_VERSION_ID >= 70100 && $type->allowsNull()) ? sprintf('?%s', $typeHint) : $typeHint; + return (!$withoutNullable && \PHP_VERSION_ID >= 70100 && $type->allowsNull()) ? self::formatNullableType($typeHint) : $typeHint; } /** @@ -151,6 +151,8 @@ private static function getLegacyTypeHint(\ReflectionParameter $param) /** * Compute the class name using legacy APIs, if possible. * + * This method MUST only be called on PHP 5. + * * @param \ReflectionParameter $param * * @return string|null @@ -181,12 +183,12 @@ private static function getLegacyClassName(\ReflectionParameter $param) * * This method MUST only be called on PHP 7+. * - * @param \ReflectionType $type - * @param string $declaringClass + * @param \ReflectionType $type + * @param \ReflectionClass $declaringClass * * @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) { @@ -198,8 +200,40 @@ private static function typeToString(\ReflectionType $type, $declaringClass) // PHP 7.0 doesn't have named types, but 7.1+ does $typeHint = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type; - // '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); } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 58d56f7a3..ba4fa85c1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,20 +4,26 @@ verbose="true" > - + + ./tests + ./tests/PHP80 + + + ./tests ./tests/PHP70 ./tests/PHP71 ./tests/PHP72 + ./tests/PHP80 - + ./tests ./tests/PHP56 - ./tests/PHP70 ./tests/PHP71 ./tests/PHP72 + ./tests/PHP80 diff --git a/tests/PHP80/Php80LanguageFeaturesTest.php b/tests/PHP80/Php80LanguageFeaturesTest.php new file mode 100644 index 000000000..fbecee555 --- /dev/null +++ b/tests/PHP80/Php80LanguageFeaturesTest.php @@ -0,0 +1,107 @@ +allows()->foo($object); + + $mock->foo($object); + } + + /** @test */ + public function it_can_mock_a_class_with_a_union_argument_type_hint() + { + $mock = mock(ArgumentUnionTypeHint::class); + $object = new ArgumentUnionTypeHint(); + $mock->allows()->foo($object); + + $mock->foo($object); + } + + /** @test */ + public function it_can_mock_a_class_with_a_parent_argument_type_hint() + { + $mock = mock(ArgumentParentTypeHint::class); + $object = new ArgumentParentTypeHint(); + $mock->allows()->foo($object); + + $mock->foo($object); + } + + /** @test */ + public function it_can_mock_a_class_with_a_mixed_return_type_hint() + { + $mock = spy(ReturnTypeMixedTypeHint::class); + + $this->assertNull($mock->foo()); + } + + /** @test */ + public function it_can_mock_a_class_with_a_union_return_type_hint() + { + $mock = spy(ReturnTypeUnionTypeHint::class); + + $this->assertTrue(is_object($mock->foo())); + } + + /** @test */ + public function it_can_mock_a_class_with_a_parent_return_type_hint() + { + $mock = spy(ReturnTypeParentTypeHint::class); + + $this->assertInstanceOf(\stdClass::class, $mock->foo()); + } +} + +class ArgumentMixedTypeHint +{ + public function foo(mixed $foo) + { + } +} + +class ArgumentUnionTypeHint +{ + public function foo(string|array|self $foo) + { + } +} + +class ArgumentParentTypeHint extends \stdClass +{ + public function foo(parent $foo) + { + } +} + +class ReturnTypeMixedTypeHint +{ + public function foo(): mixed + { + } +} + +class ReturnTypeUnionTypeHint +{ + public function foo(): ReturnTypeMixedTypeHint|self + { + } +} + +class ReturnTypeParentTypeHint extends \stdClass +{ + public function foo(): parent + { + } +}