1<?php declare(strict_types=1);
2/*
3 * This file is part of sebastian/type.
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 */
10namespace SebastianBergmann\Type;
11
12use function assert;
13use function sprintf;
14use ReflectionMethod;
15use ReflectionNamedType;
16use ReflectionType;
17use ReflectionUnionType;
18
19final class ReflectionMapper
20{
21    public function fromMethodReturnType(ReflectionMethod $method): Type
22    {
23        if (!$this->reflectionMethodHasReturnType($method)) {
24            return new UnknownType;
25        }
26
27        $returnType = $this->reflectionMethodGetReturnType($method);
28
29        assert($returnType instanceof ReflectionNamedType || $returnType instanceof ReflectionUnionType);
30
31        if ($returnType instanceof ReflectionNamedType) {
32            if ($returnType->getName() === 'self') {
33                return ObjectType::fromName(
34                    $method->getDeclaringClass()->getName(),
35                    $returnType->allowsNull()
36                );
37            }
38
39            if ($returnType->getName() === 'static') {
40                return new StaticType(
41                    TypeName::fromReflection($method->getDeclaringClass()),
42                    $returnType->allowsNull()
43                );
44            }
45
46            if ($returnType->getName() === 'mixed') {
47                return new MixedType;
48            }
49
50            if ($returnType->getName() === 'parent') {
51                $parentClass = $method->getDeclaringClass()->getParentClass();
52
53                // @codeCoverageIgnoreStart
54                if ($parentClass === false) {
55                    throw new RuntimeException(
56                        sprintf(
57                            '%s::%s() has a "parent" return type declaration but %s does not have a parent class',
58                            $method->getDeclaringClass()->getName(),
59                            $method->getName(),
60                            $method->getDeclaringClass()->getName()
61                        )
62                    );
63                }
64                // @codeCoverageIgnoreEnd
65
66                return ObjectType::fromName(
67                    $parentClass->getName(),
68                    $returnType->allowsNull()
69                );
70            }
71
72            return Type::fromName(
73                $returnType->getName(),
74                $returnType->allowsNull()
75            );
76        }
77
78        assert($returnType instanceof ReflectionUnionType);
79
80        $types = [];
81
82        foreach ($returnType->getTypes() as $type) {
83            assert($type instanceof ReflectionNamedType);
84
85            if ($type->getName() === 'self') {
86                $types[] = ObjectType::fromName(
87                    $method->getDeclaringClass()->getName(),
88                    false
89                );
90            } else {
91                $types[] = Type::fromName($type->getName(), false);
92            }
93        }
94
95        return new UnionType(...$types);
96    }
97
98    private function reflectionMethodHasReturnType(ReflectionMethod $method): bool
99    {
100        if ($method->hasReturnType()) {
101            return true;
102        }
103
104        if (!method_exists($method, 'hasTentativeReturnType')) {
105            return false;
106        }
107
108        return $method->hasTentativeReturnType();
109    }
110
111    private function reflectionMethodGetReturnType(ReflectionMethod $method): ?ReflectionType
112    {
113        if ($method->hasReturnType()) {
114            return $method->getReturnType();
115        }
116
117        if (!method_exists($method, 'getTentativeReturnType')) {
118            return null;
119        }
120
121        return $method->getTentativeReturnType();
122    }
123}
124