Skip to content

Commit

Permalink
Initial work on #5042
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianbergmann committed Sep 11, 2022
1 parent 5b49e2c commit 6d40a46
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 68 deletions.
1 change: 1 addition & 0 deletions .php-cs-fixer.dist.php
Expand Up @@ -14,6 +14,7 @@
->in(__DIR__ . '/tests/_files')
->in(__DIR__ . '/tests/end-to-end')
->in(__DIR__ . '/tests/unit')
->notName('InterfaceWithMethodReturningDisjunctiveNormalFormType.php')
->notName('*.phpt');

$config = new PhpCsFixer\Config;
Expand Down
7 changes: 7 additions & 0 deletions ChangeLog-9.5.md
Expand Up @@ -2,6 +2,12 @@

All notable changes of the PHPUnit 9.5 release series are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.

## [9.5.25] - 2022-MM-DD

### Added

* [#5042](https://github.com/sebastianbergmann/phpunit/issues/5042): Support Disjunctive Normal Form types

## [9.5.24] - 2022-08-30

### Added
Expand Down Expand Up @@ -190,6 +196,7 @@ All notable changes of the PHPUnit 9.5 release series are documented in this fil

* [#4535](https://github.com/sebastianbergmann/phpunit/issues/4535): `getMockFromWsdl()` does not handle methods that do not have parameters correctly

[9.5.25]: https://github.com/sebastianbergmann/phpunit/compare/9.5.24...9.5
[9.5.24]: https://github.com/sebastianbergmann/phpunit/compare/9.5.23...9.5.24
[9.5.23]: https://github.com/sebastianbergmann/phpunit/compare/9.5.22...9.5.23
[9.5.22]: https://github.com/sebastianbergmann/phpunit/compare/9.5.21...9.5.22
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -19,6 +19,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues"
},
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": ">=7.3",
Expand Down Expand Up @@ -46,7 +47,7 @@
"sebastian/global-state": "^5.0.1",
"sebastian/object-enumerator": "^4.0.3",
"sebastian/resource-operations": "^3.0.3",
"sebastian/type": "^3.1",
"sebastian/type": "^3.2",
"sebastian/version": "^3.0.2"
},
"config": {
Expand Down
72 changes: 7 additions & 65 deletions src/Framework/MockObject/MockMethod.php
Expand Up @@ -23,11 +23,8 @@
use function substr_count;
use function trim;
use function var_export;
use ReflectionIntersectionType;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType;
use SebastianBergmann\Template\Exception as TemplateException;
use SebastianBergmann\Template\Template;
use SebastianBergmann\Type\ReflectionMapper;
Expand Down Expand Up @@ -274,6 +271,7 @@ private function getTemplate(string $template): Template
private static function getMethodParametersForDeclaration(ReflectionMethod $method): string
{
$parameters = [];
$types = (new ReflectionMapper)->fromParameterTypes($method);

foreach ($method->getParameters() as $i => $parameter) {
$name = '$' . $parameter->getName();
Expand All @@ -285,19 +283,16 @@ private static function getMethodParametersForDeclaration(ReflectionMethod $meth
$name = '$arg' . $i;
}

$nullable = '';
$default = '';
$reference = '';
$typeDeclaration = '';
$type = null;
$typeName = null;

if ($parameter->hasType()) {
$type = $parameter->getType();
if (!$types[$i]->type()->isUnknown()) {
$typeDeclaration = $types[$i]->type()->asString() . ' ';
}

if ($type instanceof ReflectionNamedType) {
$typeName = $type->getName();
}
if ($parameter->isPassedByReference()) {
$reference = '&';
}

if ($parameter->isVariadic()) {
Expand All @@ -308,34 +303,7 @@ private static function getMethodParametersForDeclaration(ReflectionMethod $meth
$default = ' = null';
}

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

if ($typeName === 'self') {
$typeDeclaration = $method->getDeclaringClass()->getName() . ' ';
} elseif ($typeName !== null) {
$typeDeclaration = $typeName . ' ';
} elseif ($type instanceof ReflectionUnionType) {
$typeDeclaration = self::unionTypeAsString(
$type,
$method->getDeclaringClass()->getName()
);
} elseif ($type instanceof ReflectionIntersectionType) {
$typeDeclaration = self::intersectionTypeAsString($type);
}
}

if ($parameter->isPassedByReference()) {
$reference = '&';
}

$parameters[] = $nullable . $typeDeclaration . $reference . $name . $default;
$parameters[] = $typeDeclaration . $reference . $name . $default;
}

return implode(', ', $parameters);
Expand Down Expand Up @@ -409,30 +377,4 @@ private static function exportDefaultValue(ReflectionParameter $parameter): stri
}
// @codeCoverageIgnoreEnd
}

private static function unionTypeAsString(ReflectionUnionType $union, string $self): string
{
$types = [];

foreach ($union->getTypes() as $type) {
if ((string) $type === 'self') {
$types[] = $self;
} else {
$types[] = $type;
}
}

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

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

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

return implode('&', $types) . ' ';
}
}
@@ -0,0 +1,15 @@
<?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\TestFixture\MockObject;

interface InterfaceWithMethodReturningDisjunctiveNormalFormType
{
public function returnsDisjunctiveNormalFormType(): (AnInterface&AnotherInterface)|true;
}
Expand Up @@ -36,7 +36,7 @@ class MockFoo extends Foo implements PHPUnit\Framework\MockObject\MockObject
use \PHPUnit\Framework\MockObject\Method;
use \PHPUnit\Framework\MockObject\MockedCloneMethodWithVoidReturnType;

public function bar(int|bool $baz, Foo|stdClass|null $other)
public function bar(bool|int $baz, Foo|null|stdClass $other)
{
$__phpunit_arguments = [$baz, $other];
$__phpunit_count = func_num_args();
Expand Down
68 changes: 68 additions & 0 deletions tests/end-to-end/mock-objects/generator/parameter_dnf.phpt
@@ -0,0 +1,68 @@
--TEST--
\PHPUnit\Framework\MockObject\Generator::generate('Foo', [], 'MockFoo', true, true)
--SKIPIF--
<?php declare(strict_types=1);
if (version_compare('8.2.0-dev', PHP_VERSION, '>')) {
print 'skip: PHP 8.2 is required.';
}
--FILE--
<?php declare(strict_types=1);
interface A
{
}

interface B
{
}

class Foo
{
public function bar((A&B)|int|null $baz)
{
}
}

require_once __DIR__ . '/../../../../vendor/autoload.php';

$generator = new \PHPUnit\Framework\MockObject\Generator;

$mock = $generator->generate(
Foo::class,
[],
'MockFoo',
true,
true
);

print $mock->getClassCode();
--EXPECTF--
declare(strict_types=1);

class MockFoo extends Foo implements PHPUnit\Framework\MockObject\MockObject
{
use \PHPUnit\Framework\MockObject\Api;
use \PHPUnit\Framework\MockObject\Method;
use \PHPUnit\Framework\MockObject\MockedCloneMethodWithVoidReturnType;

public function bar((A&B)|int|null $baz)
{
$__phpunit_arguments = [$baz];
$__phpunit_count = func_num_args();

if ($__phpunit_count > 1) {
$__phpunit_arguments_tmp = func_get_args();

for ($__phpunit_i = 1; $__phpunit_i < $__phpunit_count; $__phpunit_i++) {
$__phpunit_arguments[] = $__phpunit_arguments_tmp[$__phpunit_i];
}
}

$__phpunit_result = $this->__phpunit_getInvocationHandler()->invoke(
new \PHPUnit\Framework\MockObject\Invocation(
'Foo', 'bar', $__phpunit_arguments, '', $this, true
)
);

return $__phpunit_result;
}
}
Expand Up @@ -36,7 +36,7 @@ class MockFoo extends Foo implements PHPUnit\Framework\MockObject\MockObject
use \PHPUnit\Framework\MockObject\Method;
use \PHPUnit\Framework\MockObject\MockedCloneMethodWithVoidReturnType;

public function bar(int|bool $baz, Foo|stdClass $other)
public function bar(bool|int $baz, Foo|stdClass $other)
{
$__phpunit_arguments = [$baz, $other];
$__phpunit_count = func_num_args();
Expand Down
@@ -0,0 +1,68 @@
--TEST--
\PHPUnit\Framework\MockObject\Generator::generate('Foo', [], 'MockFoo', true, true)
--SKIPIF--
<?php declare(strict_types=1);
if (version_compare('8.2.0-dev', PHP_VERSION, '>')) {
print 'skip: PHP 8.2 is required.';
}
--FILE--
<?php declare(strict_types=1);
interface A
{
}

interface B
{
}

class Foo
{
public function bar(): (A&B)|int|null
{
}
}

require_once __DIR__ . '/../../../bootstrap.php';

$generator = new \PHPUnit\Framework\MockObject\Generator;

$mock = $generator->generate(
Foo::class,
[],
'MockFoo',
true,
true
);

print $mock->getClassCode();
--EXPECTF--
declare(strict_types=1);

class MockFoo extends Foo implements PHPUnit\Framework\MockObject\MockObject
{
use \PHPUnit\Framework\MockObject\Api;
use \PHPUnit\Framework\MockObject\Method;
use \PHPUnit\Framework\MockObject\MockedCloneMethodWithVoidReturnType;

public function bar(): (A&B)|int|null
{
$__phpunit_arguments = [];
$__phpunit_count = func_num_args();

if ($__phpunit_count > 0) {
$__phpunit_arguments_tmp = func_get_args();

for ($__phpunit_i = 0; $__phpunit_i < $__phpunit_count; $__phpunit_i++) {
$__phpunit_arguments[] = $__phpunit_arguments_tmp[$__phpunit_i];
}
}

$__phpunit_result = $this->__phpunit_getInvocationHandler()->invoke(
new \PHPUnit\Framework\MockObject\Invocation(
'Foo', 'bar', $__phpunit_arguments, '(A&B)|int|null', $this, true
)
);

return $__phpunit_result;
}
}
13 changes: 13 additions & 0 deletions tests/unit/Framework/MockObject/MockObjectTest.php
Expand Up @@ -32,6 +32,7 @@
use PHPUnit\TestFixture\MethodCallback;
use PHPUnit\TestFixture\MethodCallbackByReference;
use PHPUnit\TestFixture\MockObject\AbstractMockTestClass;
use PHPUnit\TestFixture\MockObject\InterfaceWithMethodReturningDisjunctiveNormalFormType;
use PHPUnit\TestFixture\MockObject\InterfaceWithMethodReturningFalse;
use PHPUnit\TestFixture\MockObject\InterfaceWithMethodReturningIntersection;
use PHPUnit\TestFixture\MockObject\InterfaceWithMethodReturningNull;
Expand Down Expand Up @@ -1333,6 +1334,18 @@ public function testMethodThatReturnsTrueCanBeStubbed(): void
$this->assertTrue($i->returnsTrue());
}

/**
* @requires PHP 8.2
*/
public function testMethodThatReturnsDisjunctiveNormalFormTypeCanBeStubbed(): void
{
$i = $this->createStub(InterfaceWithMethodReturningDisjunctiveNormalFormType::class);

$i->method('returnsDisjunctiveNormalFormType')->willReturn(true);

$this->assertTrue($i->returnsDisjunctiveNormalFormType());
}

private function resetMockObjects(): void
{
$refl = new ReflectionObject($this);
Expand Down

0 comments on commit 6d40a46

Please sign in to comment.