Skip to content

Commit bf4dc84

Browse files
Closes #4932
1 parent 4a11788 commit bf4dc84

File tree

11 files changed

+381
-67
lines changed

11 files changed

+381
-67
lines changed

.psalm/baseline.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,14 @@
487487
<code>$client-&gt;__getFunctions()</code>
488488
</PossiblyNullArgument>
489489
</file>
490+
<file src="src/Framework/MockObject/Invocation.php">
491+
<ArgumentTypeCoercion occurrences="1">
492+
<code>$types</code>
493+
</ArgumentTypeCoercion>
494+
<MissingThrowsDocblock occurrences="1">
495+
<code>throw $t;</code>
496+
</MissingThrowsDocblock>
497+
</file>
490498
<file src="src/Framework/MockObject/InvocationHandler.php">
491499
<MissingReturnType occurrences="1">
492500
<code>invoke</code>

ChangeLog-9.5.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes of the PHPUnit 9.5 release series are documented in this fil
77
### Fixed
88

99
* [#4929](https://github.com/sebastianbergmann/phpunit/issues/4929): Test Double code generator does not handle new expressions inside parameter default values
10+
* [#4932](https://github.com/sebastianbergmann/phpunit/issues/4932): Backport support for intersection types from PHPUnit 10 to PHPUnit 9.5
1011
* [#4933](https://github.com/sebastianbergmann/phpunit/issues/4933): Backport support for `never` type from PHPUnit 10 to PHPUnit 9.5
1112

1213
## [9.5.18] - 2022-03-08

src/Framework/MockObject/Generator.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,67 @@ public function getMock(string $type, $methods = [], array $arguments = [], stri
171171
);
172172
}
173173

174+
/**
175+
* @psalm-param list<class-string> $interfaces
176+
*
177+
* @throws RuntimeException
178+
* @throws UnknownTypeException
179+
*/
180+
public function getMockForInterfaces(array $interfaces, bool $callAutoload = true): MockObject
181+
{
182+
if (count($interfaces) < 2) {
183+
throw new RuntimeException('At least two interfaces must be specified');
184+
}
185+
186+
foreach ($interfaces as $interface) {
187+
if (!interface_exists($interface, $callAutoload)) {
188+
throw new UnknownTypeException($interface);
189+
}
190+
}
191+
192+
sort($interfaces);
193+
194+
$methods = [];
195+
196+
foreach ($interfaces as $interface) {
197+
$methods = array_merge($methods, $this->getClassMethods($interface));
198+
}
199+
200+
if (count(array_unique($methods)) < count($methods)) {
201+
throw new RuntimeException('Interfaces must not declare the same method');
202+
}
203+
204+
$unqualifiedNames = [];
205+
206+
foreach ($interfaces as $interface) {
207+
$parts = explode('\\', $interface);
208+
$unqualifiedNames[] = array_pop($parts);
209+
}
210+
211+
sort($unqualifiedNames);
212+
213+
do {
214+
$intersectionName = sprintf(
215+
'Intersection_%s_%s',
216+
implode('_', $unqualifiedNames),
217+
substr(md5((string) mt_rand()), 0, 8)
218+
);
219+
} while (interface_exists($intersectionName, false));
220+
221+
$template = $this->getTemplate('intersection.tpl');
222+
223+
$template->setVar(
224+
[
225+
'intersection' => $intersectionName,
226+
'interfaces' => implode(', ', $interfaces),
227+
]
228+
);
229+
230+
eval($template->render());
231+
232+
return $this->getMock($intersectionName);
233+
}
234+
174235
/**
175236
* Returns a mock object for the specified abstract class with all abstract
176237
* methods of the class mocked.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
declare(strict_types=1);
2+
3+
interface {intersection} extends {interfaces}
4+
{
5+
}

src/Framework/MockObject/Invocation.php

Lines changed: 106 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -121,100 +121,141 @@ public function generateReturnValue()
121121
return null;
122122
}
123123

124-
$union = false;
124+
$intersection = false;
125+
$union = false;
125126

126127
if (strpos($this->returnType, '|') !== false) {
127128
$types = explode('|', $this->returnType);
128129
$union = true;
130+
} elseif (strpos($this->returnType, '&') !== false) {
131+
$types = explode('&', $this->returnType);
132+
$intersection = true;
129133
} else {
130134
$types = [$this->returnType];
131135
}
132136

133137
$types = array_map('strtolower', $types);
134138

135-
if (in_array('', $types, true) ||
136-
in_array('null', $types, true) ||
137-
in_array('mixed', $types, true) ||
138-
in_array('void', $types, true)) {
139-
return null;
140-
}
139+
if (!$intersection) {
140+
if (in_array('', $types, true) ||
141+
in_array('null', $types, true) ||
142+
in_array('mixed', $types, true) ||
143+
in_array('void', $types, true)) {
144+
return null;
145+
}
141146

142-
if (in_array('false', $types, true) ||
143-
in_array('bool', $types, true)) {
144-
return false;
145-
}
147+
if (in_array('false', $types, true) ||
148+
in_array('bool', $types, true)) {
149+
return false;
150+
}
146151

147-
if (in_array('float', $types, true)) {
148-
return 0.0;
149-
}
152+
if (in_array('float', $types, true)) {
153+
return 0.0;
154+
}
150155

151-
if (in_array('int', $types, true)) {
152-
return 0;
153-
}
156+
if (in_array('int', $types, true)) {
157+
return 0;
158+
}
154159

155-
if (in_array('string', $types, true)) {
156-
return '';
157-
}
160+
if (in_array('string', $types, true)) {
161+
return '';
162+
}
158163

159-
if (in_array('array', $types, true)) {
160-
return [];
161-
}
164+
if (in_array('array', $types, true)) {
165+
return [];
166+
}
162167

163-
if (in_array('static', $types, true)) {
164-
try {
165-
return (new Instantiator)->instantiate(get_class($this->object));
166-
} catch (Throwable $t) {
167-
throw new RuntimeException(
168-
$t->getMessage(),
169-
(int) $t->getCode(),
170-
$t
171-
);
168+
if (in_array('static', $types, true)) {
169+
try {
170+
return (new Instantiator)->instantiate(get_class($this->object));
171+
} catch (Throwable $t) {
172+
throw new RuntimeException(
173+
$t->getMessage(),
174+
(int) $t->getCode(),
175+
$t
176+
);
177+
}
172178
}
173-
}
174179

175-
if (in_array('object', $types, true)) {
176-
return new stdClass;
177-
}
180+
if (in_array('object', $types, true)) {
181+
return new stdClass;
182+
}
178183

179-
if (in_array('callable', $types, true) ||
180-
in_array('closure', $types, true)) {
181-
return static function (): void
182-
{
183-
};
184-
}
184+
if (in_array('callable', $types, true) ||
185+
in_array('closure', $types, true)) {
186+
return static function (): void
187+
{
188+
};
189+
}
185190

186-
if (in_array('traversable', $types, true) ||
187-
in_array('generator', $types, true) ||
188-
in_array('iterable', $types, true)) {
189-
$generator = static function (): \Generator
190-
{
191-
yield from [];
192-
};
191+
if (in_array('traversable', $types, true) ||
192+
in_array('generator', $types, true) ||
193+
in_array('iterable', $types, true)) {
194+
$generator = static function (): \Generator
195+
{
196+
yield from [];
197+
};
193198

194-
return $generator();
195-
}
199+
return $generator();
200+
}
201+
202+
if (!$union) {
203+
try {
204+
return (new Generator)->getMock($this->returnType, [], [], '', false);
205+
} catch (Throwable $t) {
206+
if ($t instanceof Exception) {
207+
throw $t;
208+
}
196209

197-
if (!$union) {
198-
try {
199-
return (new Generator)->getMock($this->returnType, [], [], '', false);
200-
} catch (Throwable $t) {
201-
throw new RuntimeException(
202-
sprintf(
203-
'Return value for %s::%s() cannot be generated: %s',
204-
$this->className,
205-
$this->methodName,
210+
throw new RuntimeException(
206211
$t->getMessage(),
207-
),
208-
(int) $t->getCode(),
209-
);
212+
(int) $t->getCode(),
213+
$t
214+
);
215+
}
216+
}
217+
}
218+
219+
$reason = '';
220+
221+
if ($union) {
222+
$reason = ' because the declared return type is a union';
223+
} elseif ($intersection) {
224+
$reason = ' because the declared return type is an intersection';
225+
226+
$onlyInterfaces = true;
227+
228+
foreach ($types as $type) {
229+
if (!interface_exists($type)) {
230+
$onlyInterfaces = false;
231+
232+
break;
233+
}
234+
}
235+
236+
if ($onlyInterfaces) {
237+
try {
238+
return (new Generator)->getMockForInterfaces($types);
239+
} catch (Throwable $t) {
240+
throw new RuntimeException(
241+
sprintf(
242+
'Return value for %s::%s() cannot be generated: %s',
243+
$this->className,
244+
$this->methodName,
245+
$t->getMessage(),
246+
),
247+
(int) $t->getCode(),
248+
);
249+
}
210250
}
211251
}
212252

213253
throw new RuntimeException(
214254
sprintf(
215-
'Return value for %s::%s() cannot be generated because the declared return type is a union, please configure a return value for this method',
255+
'Return value for %s::%s() cannot be generated%s, please configure a return value for this method',
216256
$this->className,
217-
$this->methodName
257+
$this->methodName,
258+
$reason
218259
)
219260
);
220261
}

src/Framework/MockObject/MockMethod.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use function substr_count;
2424
use function trim;
2525
use function var_export;
26+
use ReflectionIntersectionType;
2627
use ReflectionMethod;
2728
use ReflectionNamedType;
2829
use ReflectionParameter;
@@ -32,7 +33,6 @@
3233
use SebastianBergmann\Type\ReflectionMapper;
3334
use SebastianBergmann\Type\Type;
3435
use SebastianBergmann\Type\UnknownType;
35-
use SebastianBergmann\Type\VoidType;
3636

3737
/**
3838
* @internal This class is not covered by the backward compatibility promise for PHPUnit
@@ -309,7 +309,7 @@ private static function getMethodParametersForDeclaration(ReflectionMethod $meth
309309
}
310310

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

@@ -322,6 +322,8 @@ private static function getMethodParametersForDeclaration(ReflectionMethod $meth
322322
$type,
323323
$method->getDeclaringClass()->getName()
324324
);
325+
} elseif ($type instanceof ReflectionIntersectionType) {
326+
$typeDeclaration = self::intersectionTypeAsString($type);
325327
}
326328
}
327329

@@ -418,4 +420,15 @@ private static function unionTypeAsString(ReflectionUnionType $union, string $se
418420

419421
return implode('|', $types) . ' ';
420422
}
423+
424+
private static function intersectionTypeAsString(ReflectionIntersectionType $intersection): string
425+
{
426+
$types = [];
427+
428+
foreach ($intersection->getTypes() as $type) {
429+
$types[] = $type;
430+
}
431+
432+
return implode('&', $types) . ' ';
433+
}
421434
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace PHPUnit\TestFixture\MockObject;
11+
12+
interface InterfaceWithMethodReturningIntersection
13+
{
14+
public function method(): AnInterface & AnotherInterface;
15+
}

0 commit comments

Comments
 (0)