Skip to content

Commit 1aa2cb5

Browse files
committed
WIP 2
1 parent efb6921 commit 1aa2cb5

File tree

11 files changed

+432
-42
lines changed

11 files changed

+432
-42
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
],
1919

2020
"require": {
21-
"php": "8.1.* || 8.2.* || 8.3.* || 8.4.*",
21+
"php": "8.2.* || 8.3.* || 8.4.*",
2222
"phpdocumentor/reflection-docblock": "^5.2",
2323
"sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
2424
"doctrine/instantiator": "^1.2 || ^2.0",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace spec\Prophecy\Doubler\Generator\Node\Type;
4+
5+
use PhpSpec\ObjectBehavior;
6+
use Prophecy\Doubler\Generator\Node\Type\SimpleType;
7+
use Prophecy\Doubler\Generator\Node\Type\TypeInterface;
8+
use Prophecy\Doubler\Generator\Node\Type\UnionType;
9+
use Prophecy\Exception\Doubler\DoubleException;
10+
11+
class IntersectionTypeSpec extends ObjectBehavior
12+
{
13+
function let(): void
14+
{
15+
$this->beConstructedWith([
16+
new SimpleType('Foo'),
17+
new SimpleType('Bar')
18+
]);
19+
}
20+
21+
function it_should_implement_type_union(): void
22+
{
23+
$this->shouldImplement(TypeInterface::class);
24+
}
25+
26+
function it_should_throw_double_exception_for_builtin_types()
27+
{
28+
$this->beConstructedWith([
29+
new SimpleType('string'),
30+
new SimpleType('Foo')
31+
]);
32+
$this->shouldThrow(DoubleException::class)->duringInstantiation();
33+
}
34+
35+
function it_should_throw_double_exception_if_less_than_2_types_provided()
36+
{
37+
$this->beConstructedWith([
38+
new SimpleType('Bar')
39+
]);
40+
$this->shouldThrow(DoubleException::class)->duringInstantiation();
41+
}
42+
43+
function it_should_throw_double_exception_if_union_type_given(): void
44+
{
45+
$this->beConstructedWith([
46+
new SimpleType('Bar'),
47+
new UnionType([new SimpleType('Foo'), new SimpleType('Baz')])
48+
]);
49+
$this->shouldThrow(DoubleException::class)->duringInstantiation();
50+
}
51+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace spec\Prophecy\Doubler\Generator\Node\Type;
4+
5+
use PhpSpec\ObjectBehavior;
6+
use Prophecy\Doubler\Generator\Node\Type\TypeInterface;
7+
8+
class SimpleTypeSpec extends ObjectBehavior
9+
{
10+
function let(): void
11+
{
12+
$this->beConstructedWith('string');
13+
}
14+
15+
function it_implements_type_interface(): void
16+
{
17+
$this->shouldImplement(TypeInterface::class);
18+
}
19+
20+
function it_is_stringable(): void
21+
{
22+
$this->beConstructedWith('int');
23+
$this->getType()->shouldReturn('int');
24+
$this->__toString()->shouldReturn('int');
25+
}
26+
27+
function it_prefix_namespace_with_antislash(): void
28+
{
29+
$this->beConstructedWith('Prophecy\\Doubler\\Generator\\Node\\Type\\SimpleType');
30+
$this->getType()->shouldReturn('\\Prophecy\\Doubler\\Generator\\Node\\Type\\SimpleType');
31+
$this->isBuiltin()->shouldReturn(false);
32+
}
33+
34+
function it_resolves_builtin_aliases(): void
35+
{
36+
$this->beConstructedWith('double');
37+
$this->getType()->shouldReturn('float');
38+
$this->isBuiltin()->shouldReturn(true);
39+
}
40+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace spec\Prophecy\Doubler\Generator\Node\Type;
4+
5+
use PhpSpec\ObjectBehavior;
6+
use Prophecy\Doubler\Generator\Node\Type\SimpleType;
7+
use Prophecy\Doubler\Generator\Node\Type\TypeInterface;
8+
use Prophecy\Doubler\Generator\Node\Type\UnionType;
9+
use Prophecy\Exception\Doubler\DoubleException;
10+
11+
class UnionTypeSpec extends ObjectBehavior
12+
{
13+
function let(): void
14+
{
15+
$this->beConstructedWith([
16+
new SimpleType('int'),
17+
new SimpleType('string')
18+
]);
19+
}
20+
function it_implements_type_interface(): void
21+
{
22+
$this->shouldImplement(TypeInterface::class);
23+
}
24+
25+
function it_throws_double_exception_when_union_type_given(): void
26+
{
27+
$this->beConstructedWith([
28+
new UnionType([new SimpleType('int'), new SimpleType('string')]),
29+
new SimpleType('bool')
30+
]);
31+
$this->shouldThrow(DoubleException::class)->duringInstantiation();
32+
}
33+
34+
function it_throws_double_exception_when_types_duplicated(): void
35+
{
36+
$this->beConstructedWith([new SimpleType('string'), new SimpleType('string')]);
37+
$this->shouldThrow(DoubleException::class)->duringInstantiation();
38+
}
39+
40+
function it_throws_double_exception_when_union_with_void(): void
41+
{
42+
$this->beConstructedWith([new SimpleType('void'), new SimpleType('string')]);
43+
$this->shouldThrow(DoubleException::class)->duringInstantiation();
44+
}
45+
46+
function it_throws_double_exception_when_union_with_never(): void
47+
{
48+
$this->beConstructedWith([new SimpleType('never'), new SimpleType('string')]);
49+
$this->shouldThrow(DoubleException::class)->duringInstantiation();
50+
}
51+
52+
function it_throws_double_exception_when_union_with_mixed(): void
53+
{
54+
$this->beConstructedWith([new SimpleType('mixed'), new SimpleType('string')]);
55+
$this->shouldThrow(DoubleException::class)->duringInstantiation();
56+
}
57+
58+
function it_throws_double_exception_when_union_with_only_one_type(): void
59+
{
60+
$this->beConstructedWith([new SimpleType('string')]);
61+
$this->shouldThrow(DoubleException::class)->duringInstantiation();
62+
}
63+
64+
function it_return_array_of_its_types(): void
65+
{
66+
$this->getTypes()->shouldBeLike([
67+
new SimpleType('int'),
68+
new SimpleType('string')
69+
]);
70+
}
71+
}

src/Prophecy/Doubler/Generator/ClassMirror.php

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
use Prophecy\Doubler\Generator\Node\ArgumentTypeNode;
1515
use Prophecy\Doubler\Generator\Node\ReturnTypeNode;
16+
use Prophecy\Doubler\Generator\Node\Type\IntersectionType;
17+
use Prophecy\Doubler\Generator\Node\Type\TypeInterface;
18+
use Prophecy\Doubler\Generator\Node\Type\SimpleType;
19+
use Prophecy\Doubler\Generator\Node\Type\UnionType;
1620
use Prophecy\Exception\InvalidArgumentException;
1721
use Prophecy\Exception\Doubler\ClassMirrorException;
1822
use ReflectionClass;
@@ -158,14 +162,22 @@ private function reflectMethodToNode(ReflectionMethod $method, Node\ClassNode $c
158162
$node->setReturnsReference();
159163
}
160164

165+
$returnReflectionType = null;
161166
if ($method->hasReturnType()) {
162-
\assert($method->getReturnType() !== null);
163-
$returnTypes = $this->getTypeHints($method->getReturnType(), $method->getDeclaringClass(), $method->getReturnType()->allowsNull());
164-
$node->setReturnTypeNode(new ReturnTypeNode(...$returnTypes));
167+
$returnReflectionType = $method->getReturnType();
168+
\assert($returnReflectionType !== null);
165169
} elseif (method_exists($method, 'hasTentativeReturnType') && $method->hasTentativeReturnType()) {
166-
\assert($method->getTentativeReturnType() !== null);
167-
$returnTypes = $this->getTypeHints($method->getTentativeReturnType(), $method->getDeclaringClass(), $method->getTentativeReturnType()->allowsNull());
168-
$node->setReturnTypeNode(new ReturnTypeNode(...$returnTypes));
170+
// Tentative return types also need reflection
171+
$returnReflectionType = $method->getTentativeReturnType();
172+
\assert($returnReflectionType !== null);
173+
}
174+
175+
if (null !== $returnReflectionType) {
176+
$returnType = $this->createTypeFromReflection(
177+
$returnReflectionType,
178+
$method->getDeclaringClass()
179+
);
180+
$node->setReturnTypeNode(new ReturnTypeNode($returnType));
169181
}
170182

171183
if (is_array($params = $method->getParameters()) && count($params)) {
@@ -232,6 +244,74 @@ private function getDefaultValue(ReflectionParameter $parameter)
232244
return $parameter->getDefaultValue();
233245
}
234246

247+
/**
248+
* @param ReflectionClass<object> $declaringClass Context reflection class
249+
*/
250+
private function createTypeFromReflection(ReflectionType $type, ReflectionClass $declaringClass): TypeInterface
251+
{
252+
if ($type instanceof ReflectionIntersectionType) {
253+
foreach ($type->getTypes() as $innerReflectionType) {
254+
$innerTypes[] = new SimpleType($innerReflectionType->getName());
255+
}
256+
return new IntersectionType($innerTypes);
257+
}
258+
259+
if ($type instanceof ReflectionUnionType) {
260+
$innerTypes = [];
261+
foreach ($type->getTypes() as $innerReflectionType) {
262+
if ($innerReflectionType instanceof ReflectionIntersectionType) {
263+
$innerTypes[] = $this->createTypeFromReflection($innerReflectionType, $declaringClass);
264+
continue;
265+
}
266+
$name = $this->resolveTypeName($innerReflectionType->getName(), $declaringClass);
267+
$innerTypes[] = new SimpleType($name);
268+
}
269+
// Nullability is handled by 'null' being one of the types in the union
270+
return new UnionType($innerTypes);
271+
}
272+
273+
// Handle Named Types (single types like int, string, MyClass, ?MyClass)
274+
if ($type instanceof ReflectionNamedType) {
275+
$name = $this->resolveTypeName($type->getName(), $declaringClass);
276+
$simpleType = new SimpleType($name); // SimpleType constructor normalizes
277+
278+
// Handle nullability for named types explicitly by wrapping in a UnionType if needed
279+
if ($type->allowsNull() && $name !== 'mixed' && $name !== 'null') {
280+
// Check if SimpleType already resolved to 'null' (e.g. input was 'null')
281+
if ((string)$simpleType === 'null') {
282+
return $simpleType; // Already null, no union needed
283+
}
284+
// Check if SimpleType already resolved to 'mixed'
285+
if ((string)$simpleType === 'mixed') {
286+
return $simpleType; // mixed implies null, no union needed
287+
}
288+
289+
return new UnionType([$simpleType, new SimpleType('null')]);
290+
}
291+
292+
return $simpleType;
293+
}
294+
295+
// Unknown ReflectionType implementation
296+
throw new ClassMirrorException('Unknown reflection type: ' . get_class($type), $declaringClass);
297+
}
298+
299+
private function resolveTypeName(string $name, ReflectionClass $contextClass): string
300+
{
301+
if ($name === 'self') {
302+
return $contextClass->getName();
303+
}
304+
if ($name === 'parent') {
305+
$parent = $contextClass->getParentClass();
306+
if (false === $parent) {
307+
throw new ClassMirrorException(sprintf('Cannot use "parent" type hint in class "%s" as it does not have a parent.', $contextClass->getName()), $contextClass);
308+
}
309+
return $parent->getName();
310+
}
311+
312+
return $name;
313+
}
314+
235315
/**
236316
* @param ReflectionClass<object> $class
237317
*

src/Prophecy/Doubler/Generator/Node/Type/AbstractType.php

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/Prophecy/Doubler/Generator/Node/Type/IntersectionType.php

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,64 @@
22

33
namespace Prophecy\Doubler\Generator\Node\Type;
44

5-
class IntersectionType extends AbstractType
5+
use Prophecy\Exception\Doubler\DoubleException;
6+
7+
class IntersectionType implements TypeInterface
68
{
9+
/**
10+
* @param list<SimpleType> $types
11+
*/
12+
public function __construct(private array $types)
13+
{
14+
$this->guard();
15+
}
16+
17+
/**
18+
* @return list<TypeInterface>
19+
*/
20+
public function getTypes(): array
21+
{
22+
return $this->types;
23+
}
24+
25+
private function has(SimpleType $givenType): bool
26+
{
27+
foreach ($this->types as $type) {
28+
if ($type->equals($givenType)) {
29+
return true;
30+
}
31+
}
32+
33+
return false;
34+
}
35+
36+
public function equals(TypeInterface $givenType): bool
37+
{
38+
if (!$givenType instanceof IntersectionType) {
39+
return false;
40+
}
41+
42+
if (count($this->types) !== count($givenType->getTypes())) {
43+
return false;
44+
}
45+
46+
foreach ($this->types as $type) {
47+
if (!$givenType->has($type)) {
48+
return false;
49+
}
50+
}
51+
}
52+
753
private function guard(): void
854
{
9-
// Cannot contain void, never or union
55+
// Cannot contain void, never, null, scalar types, mixed, union types etc.
56+
foreach ($this->types as $type) {
57+
if (!$type instanceof SimpleType || $type->isBuiltin()) {
58+
throw new DoubleException('Intersection types can only contain class/interface names.');
59+
}
60+
}
61+
if (count($this->types) < 2) {
62+
throw new DoubleException('Intersection types must contain at least two types.');
63+
}
1064
}
1165
}

0 commit comments

Comments
 (0)