From 71cf3415e3c81842b4b844d1bff8af9df7e5e8a7 Mon Sep 17 00:00:00 2001 From: kschatzle Date: Mon, 26 Sep 2022 08:46:06 -0500 Subject: [PATCH 1/3] Intersection types in generated code Adding void return type fixture and test. --- fixtures/VoidReturnType.php | 11 +++++++++++ tests/Doubler/Generator/ClassMirrorTest.php | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 fixtures/VoidReturnType.php diff --git a/fixtures/VoidReturnType.php b/fixtures/VoidReturnType.php new file mode 100644 index 000000000..b84565ae8 --- /dev/null +++ b/fixtures/VoidReturnType.php @@ -0,0 +1,11 @@ +reflect(new \ReflectionClass('Fixtures\Prophecy\VoidReturnType'), []); + $methodNode = $classNode->getMethods()['doSomething']; + + $this->assertEquals(new ReturnTypeNode('void'), $methodNode->getReturnTypeNode()); + } + /** * @test */ From 8d3935b8aa1484a1d356dffa6dd67122a5bebf3b Mon Sep 17 00:00:00 2001 From: kschatzle Date: Mon, 26 Sep 2022 12:52:56 -0500 Subject: [PATCH 2/3] Intersection types in generated code Removing naming normalization of types from node to ClassMirror.php. --- .../Generator/ClassCodeGeneratorSpec.php | 7 +- .../Generator/Node/ArgumentTypeNodeSpec.php | 7 -- .../ArgumentTypeNameNormalizationSpec.php | 43 +++++++++++ .../ReturnTypeNameNormalizationSpec.php | 53 +++++++++++++ .../Generator/Node/ReturnTypeNodeSpec.php | 8 +- .../ClassPatch/ProphecySubjectPatch.php | 2 +- .../Doubler/Generator/ClassMirror.php | 19 ++++- .../Generator/Node/NameNormalization.php | 8 ++ .../ArgumentTypeNameNormalization.php | 8 ++ .../NameNormalizationAbstract.php | 58 +++++++++++++++ .../ReturnTypeNameNormalization.php | 17 +++++ .../Doubler/Generator/Node/ReturnTypeNode.php | 21 ++---- .../Generator/Node/TypeNodeAbstract.php | 74 +++++++------------ tests/Doubler/Generator/ClassMirrorTest.php | 12 +-- 14 files changed, 251 insertions(+), 86 deletions(-) create mode 100644 spec/Prophecy/Doubler/Generator/Node/NameNormalization/ArgumentTypeNameNormalizationSpec.php create mode 100644 spec/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalizationSpec.php create mode 100644 src/Prophecy/Doubler/Generator/Node/NameNormalization.php create mode 100644 src/Prophecy/Doubler/Generator/Node/NameNormalization/ArgumentTypeNameNormalization.php create mode 100644 src/Prophecy/Doubler/Generator/Node/NameNormalization/NameNormalizationAbstract.php create mode 100644 src/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalization.php diff --git a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php index 0031fe809..8d78e8db2 100644 --- a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php +++ b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php @@ -84,7 +84,7 @@ function it_generates_proper_php_code_for_specific_ClassNode( $argument12->isOptional()->willReturn(false); $argument12->isPassedByReference()->willReturn(false); $argument12->isVariadic()->willReturn(false); - $argument12->getTypeNode()->willReturn(new ArgumentTypeNode('ReflectionClass')); + $argument12->getTypeNode()->willReturn(new ArgumentTypeNode('\ReflectionClass')); $argument13->getName()->willReturn('instance'); $argument13->isOptional()->willReturn(false); @@ -135,6 +135,7 @@ public function returnObject(): object { } PHP; + $expected = strtr($expected, array("\r\n" => "\n", "\r" => "\n")); $code->shouldBe($expected); } @@ -205,13 +206,13 @@ function it_generates_proper_php_code_for_variadics( $argument3->isOptional()->willReturn(false); $argument3->isPassedByReference()->willReturn(false); $argument3->isVariadic()->willReturn(true); - $argument3->getTypeNode()->willReturn(new ArgumentTypeNode('ReflectionClass')); + $argument3->getTypeNode()->willReturn(new ArgumentTypeNode('\ReflectionClass')); $argument4->getName()->willReturn('args'); $argument4->isOptional()->willReturn(false); $argument4->isPassedByReference()->willReturn(true); $argument4->isVariadic()->willReturn(true); - $argument4->getTypeNode()->willReturn(new ArgumentTypeNode('ReflectionClass')); + $argument4->getTypeNode()->willReturn(new ArgumentTypeNode('\ReflectionClass')); $code = $this->generate('CustomClass', $class); diff --git a/spec/Prophecy/Doubler/Generator/Node/ArgumentTypeNodeSpec.php b/spec/Prophecy/Doubler/Generator/Node/ArgumentTypeNodeSpec.php index 9f9f6bbbe..1c3696ec9 100644 --- a/spec/Prophecy/Doubler/Generator/Node/ArgumentTypeNodeSpec.php +++ b/spec/Prophecy/Doubler/Generator/Node/ArgumentTypeNodeSpec.php @@ -27,13 +27,6 @@ function it_can_have_multiple_types() $this->getTypes()->shouldReturn(['int', 'string']); } - function it_will_prefix_fcqns() - { - $this->beConstructedWith('Foo'); - - $this->getTypes()->shouldReturn(['\\Foo']); - } - function it_will_not_prefix_fcqns_that_already_have_prefix() { $this->beConstructedWith('\\Foo'); diff --git a/spec/Prophecy/Doubler/Generator/Node/NameNormalization/ArgumentTypeNameNormalizationSpec.php b/spec/Prophecy/Doubler/Generator/Node/NameNormalization/ArgumentTypeNameNormalizationSpec.php new file mode 100644 index 000000000..f50245383 --- /dev/null +++ b/spec/Prophecy/Doubler/Generator/Node/NameNormalization/ArgumentTypeNameNormalizationSpec.php @@ -0,0 +1,43 @@ +normalize()->shouldReturn([]); + } + + function it_can_have_a_simple_type() + { + $this->normalize('int')->shouldReturn(['int']); + } + + function it_can_have_multiple_types() + { + $this->normalize('int', 'string')->shouldReturn(['int', 'string']); + } + + function it_will_prefix_fcqns() + { + $this->normalize('Foo')->shouldReturn(['\\Foo']); + } + + function it_will_not_prefix_fcqns_that_already_have_prefix() + { + $this->beConstructedWith(); + + $this->normalize('\\Foo')->shouldReturn(['\\Foo']); + } + + function it_does_not_prefix_false() + { + $this->beConstructedWith(); + + $this->normalize('false', 'array')->shouldReturn(['false', 'array']); + } +} diff --git a/spec/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalizationSpec.php b/spec/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalizationSpec.php new file mode 100644 index 000000000..2dee3f27d --- /dev/null +++ b/spec/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalizationSpec.php @@ -0,0 +1,53 @@ +normalize()->shouldReturn([]); + } + + function it_can_have_a_simple_type() + { + $this->normalize('int')->shouldReturn(['int']); + } + + function it_can_have_multiple_types() + { + $this->normalize('int', 'string')->shouldReturn(['int', 'string']); + } + + function it_can_have_void_type() + { + $this->normalize('void')->shouldReturn(['void']); + } + + function it_will_normalise_type_aliases_types() + { + $this->normalize('double', 'real', 'boolean', 'integer')->shouldReturn(['float', 'bool', 'int']); + } + + function it_will_prefix_fcqns() + { + $this->normalize('Foo')->shouldReturn(['\\Foo']); + } + + function it_will_not_prefix_fcqns_that_already_have_prefix() + { + $this->normalize('\\Foo')->shouldReturn(['\\Foo']); + } + + function it_does_not_prefix_false() + { + $this->normalize('false', 'array')->shouldReturn(['false', 'array']); + } + + function it_does_not_prefix_never() + { + $this->normalize('never')->shouldReturn(['never']); + } +} diff --git a/spec/Prophecy/Doubler/Generator/Node/ReturnTypeNodeSpec.php b/spec/Prophecy/Doubler/Generator/Node/ReturnTypeNodeSpec.php index 7670339db..d93770490 100644 --- a/spec/Prophecy/Doubler/Generator/Node/ReturnTypeNodeSpec.php +++ b/spec/Prophecy/Doubler/Generator/Node/ReturnTypeNodeSpec.php @@ -33,18 +33,18 @@ function it_can_have_void_type() $this->getTypes()->shouldReturn(['void']); } - function it_will_normalise_type_aliases_types() + function it_will_not_normalise_type_aliases_types() { $this->beConstructedWith('double', 'real', 'boolean', 'integer'); - $this->getTypes()->shouldReturn(['float', 'bool', 'int']); + $this->getTypes()->shouldNotBe(['float', 'bool', 'int']); } - function it_will_prefix_fcqns() + function it_will_not_prefix_fcqns() { $this->beConstructedWith('Foo'); - $this->getTypes()->shouldReturn(['\\Foo']); + $this->getTypes()->shouldNotBe(['\\Foo']); } function it_will_not_prefix_fcqns_that_already_have_prefix() diff --git a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php index 7573ca50e..92394c73c 100644 --- a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php +++ b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php @@ -65,7 +65,7 @@ public function apply(ClassNode $node) $prophecySetter = new MethodNode('setProphecy'); $prophecyArgument = new ArgumentNode('prophecy'); - $prophecyArgument->setTypeNode(new ArgumentTypeNode('Prophecy\Prophecy\ProphecyInterface')); + $prophecyArgument->setTypeNode(new ArgumentTypeNode('\Prophecy\Prophecy\ProphecyInterface')); $prophecySetter->addArgument($prophecyArgument); $prophecySetter->setCode(<<objectProphecyClosure) { diff --git a/src/Prophecy/Doubler/Generator/ClassMirror.php b/src/Prophecy/Doubler/Generator/ClassMirror.php index 5d9cd2d20..55c174940 100644 --- a/src/Prophecy/Doubler/Generator/ClassMirror.php +++ b/src/Prophecy/Doubler/Generator/ClassMirror.php @@ -12,6 +12,8 @@ namespace Prophecy\Doubler\Generator; use Prophecy\Doubler\Generator\Node\ArgumentTypeNode; +use Prophecy\Doubler\Generator\Node\NameNormalization\ArgumentTypeNameNormalization; +use Prophecy\Doubler\Generator\Node\NameNormalization\ReturnTypeNameNormalization; use Prophecy\Doubler\Generator\Node\ReturnTypeNode; use Prophecy\Exception\InvalidArgumentException; use Prophecy\Exception\Doubler\ClassMirrorException; @@ -150,10 +152,18 @@ private function reflectMethodToNode(ReflectionMethod $method, Node\ClassNode $c if ($method->hasReturnType()) { $returnTypes = $this->getTypeHints($method->getReturnType(), $method->getDeclaringClass(), $method->getReturnType()->allowsNull()); + + $normalization = new ReturnTypeNameNormalization(); + $returnTypes = $normalization->normalize(...$returnTypes); + $node->setReturnTypeNode(new ReturnTypeNode(...$returnTypes)); } elseif (method_exists($method, 'hasTentativeReturnType') && $method->hasTentativeReturnType()) { $returnTypes = $this->getTypeHints($method->getTentativeReturnType(), $method->getDeclaringClass(), $method->getTentativeReturnType()->allowsNull()); + + $normalization = new ReturnTypeNameNormalization(); + $returnTypes = $normalization->normalize(...$returnTypes); + $node->setReturnTypeNode(new ReturnTypeNode(...$returnTypes)); } @@ -171,9 +181,14 @@ private function reflectArgumentToNode(ReflectionParameter $parameter, Node\Meth $name = $parameter->getName() == '...' ? '__dot_dot_dot__' : $parameter->getName(); $node = new Node\ArgumentNode($name); - $typeHints = $this->getTypeHints($parameter->getType(), $parameter->getDeclaringClass(), $parameter->allowsNull()); + if ($parameter->hasType()) { + $typeHints = $this->getTypeHints($parameter->getType(), $parameter->getDeclaringClass(), $parameter->allowsNull()); + + $normalization = new ArgumentTypeNameNormalization(); + $typeHints = $normalization->normalize(...$typeHints); - $node->setTypeNode(new ArgumentTypeNode(...$typeHints)); + $node->setTypeNode(new ArgumentTypeNode(...$typeHints)); + } if ($parameter->isVariadic()) { $node->setAsVariadic(); diff --git a/src/Prophecy/Doubler/Generator/Node/NameNormalization.php b/src/Prophecy/Doubler/Generator/Node/NameNormalization.php new file mode 100644 index 000000000..9188eb19d --- /dev/null +++ b/src/Prophecy/Doubler/Generator/Node/NameNormalization.php @@ -0,0 +1,8 @@ +getRealType($type); + $normalizedTypes[$type] = $type; + } + + return array_values($normalizedTypes); + } + + protected function getRealType(string $type): string + { + switch ($type) { + // type aliases + case 'double': + case 'real': + return 'float'; + case 'boolean': + return 'bool'; + case 'integer': + return 'int'; + + // built in types + case 'self': + case 'static': + case 'array': + case 'callable': + case 'bool': + case 'false': + case 'float': + case 'int': + case 'string': + case 'iterable': + case 'object': + case 'null': + return $type; + case 'mixed': + return \PHP_VERSION_ID < 80000 ? $this->prefixWithNsSeparator($type) : $type; + + default: + return $this->prefixWithNsSeparator($type); + } + } + + public function prefixWithNsSeparator(string $type): string + { + return '\\' . ltrim($type, '\\'); + } +} \ No newline at end of file diff --git a/src/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalization.php b/src/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalization.php new file mode 100644 index 000000000..bbf9c42ab --- /dev/null +++ b/src/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalization.php @@ -0,0 +1,17 @@ +types['void']) && count($this->types) !== 1) { + if (in_array('void', $this->types) && count($this->types) !== 1) { throw new DoubleException('void cannot be part of a union'); } - if (isset($this->types['never']) && count($this->types) !== 1) { + if (in_array('never', $this->types) && count($this->types) !== 1) { throw new DoubleException('never cannot be part of a union'); } @@ -34,12 +23,12 @@ protected function guardIsValidType() */ public function isVoid() { - return $this->types == ['void' => 'void']; + return $this->types == ['void']; } public function hasReturnStatement(): bool { - return $this->types !== ['void' => 'void'] - && $this->types !== ['never' => 'never']; + return $this->types !== ['void'] + && $this->types !== ['never']; } } diff --git a/src/Prophecy/Doubler/Generator/Node/TypeNodeAbstract.php b/src/Prophecy/Doubler/Generator/Node/TypeNodeAbstract.php index 97fc54978..f0e5cc597 100644 --- a/src/Prophecy/Doubler/Generator/Node/TypeNodeAbstract.php +++ b/src/Prophecy/Doubler/Generator/Node/TypeNodeAbstract.php @@ -11,17 +11,13 @@ abstract class TypeNodeAbstract public function __construct(string ...$types) { - foreach ($types as $type) { - $type = $this->getRealType($type); - $this->types[$type] = $type; - } - + $this->types = $types; $this->guardIsValidType(); } public function canUseNullShorthand(): bool { - return isset($this->types['null']) && count($this->types) <= 2; + return in_array('null', $this->types) && count($this->types) <= 2; } public function getTypes(): array @@ -32,65 +28,49 @@ public function getTypes(): array public function getNonNullTypes(): array { $nonNullTypes = $this->types; - unset($nonNullTypes['null']); + + if (($key = array_search('null', $nonNullTypes)) !== false) { + unset($nonNullTypes[$key]); + } return array_values($nonNullTypes); } - protected function prefixWithNsSeparator(string $type): string - { - return '\\' . ltrim($type, '\\'); + /** + * Order of array does not matter. $array has to be non empty. + * + * @param $array + * @return bool + */ + protected function doesArrayEqual($array) + { + if (empty($this->types)) { + return false; + } + $intersection = array_intersect($this->types, $array); + + return count($intersection) == count($this->types); } - protected function getRealType(string $type): string + protected function guardIsValidType() { - switch ($type) { - // type aliases - case 'double': - case 'real': - return 'float'; - case 'boolean': - return 'bool'; - case 'integer': - return 'int'; - - // built in types - case 'self': - case 'static': - case 'array': - case 'callable': - case 'bool': - case 'false': - case 'float': - case 'int': - case 'string': - case 'iterable': - case 'object': - case 'null': - return $type; - case 'mixed': - return \PHP_VERSION_ID < 80000 ? $this->prefixWithNsSeparator($type) : $type; - - default: - return $this->prefixWithNsSeparator($type); + if(!empty($this->types) && count(array_intersect($this->types, ['false', 'null'])) == count($this->types)){ + throw new DoubleException('Type cannot be nullable false'); } - } - protected function guardIsValidType() - { - if ($this->types == ['null' => 'null']) { + if ($this->doesArrayEqual(['null'])) { throw new DoubleException('Type cannot be standalone null'); } - if ($this->types == ['false' => 'false']) { + if ($this->doesArrayEqual(['false'])) { throw new DoubleException('Type cannot be standalone false'); } - if ($this->types == ['false' => 'false', 'null' => 'null']) { + if ($this->doesArrayEqual(['false', 'null'])) { throw new DoubleException('Type cannot be nullable false'); } - if (\PHP_VERSION_ID >= 80000 && isset($this->types['mixed']) && count($this->types) !== 1) { + if (\PHP_VERSION_ID >= 80000 && in_array('mixed', $this->types) && count($this->types) !== 1) { throw new DoubleException('mixed cannot be part of a union'); } } diff --git a/tests/Doubler/Generator/ClassMirrorTest.php b/tests/Doubler/Generator/ClassMirrorTest.php index 9c36605e9..c2ab9d944 100644 --- a/tests/Doubler/Generator/ClassMirrorTest.php +++ b/tests/Doubler/Generator/ClassMirrorTest.php @@ -103,7 +103,7 @@ public function it_properly_reads_methods_arguments_with_types() $this->assertEquals('arg_1', $argNodes[0]->getName()); - $this->assertEquals(new ArgumentTypeNode('ArrayAccess'), $argNodes[0]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode('\ArrayAccess'), $argNodes[0]->getTypeNode()); $this->assertFalse($argNodes[0]->isOptional()); $this->assertEquals('arg_2', $argNodes[1]->getName()); @@ -114,7 +114,7 @@ public function it_properly_reads_methods_arguments_with_types() $this->assertFalse($argNodes[1]->isVariadic()); $this->assertEquals('arg_3', $argNodes[2]->getName()); - $this->assertEquals(new ArgumentTypeNode('ArrayAccess', 'null'), $argNodes[2]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode('\ArrayAccess', 'null'), $argNodes[2]->getTypeNode()); $this->assertTrue($argNodes[2]->isOptional()); $this->assertNull($argNodes[2]->getDefault()); $this->assertFalse($argNodes[2]->isPassedByReference()); @@ -413,7 +413,7 @@ public function it_doesnt_fail_to_typehint_nonexistent_FQCN() $classNode = $mirror->reflect(new \ReflectionClass('Fixtures\Prophecy\OptionalDepsClass'), array()); $method = $classNode->getMethod('iHaveAStrangeTypeHintedArg'); $arguments = $method->getArguments(); - $this->assertEquals(new ArgumentTypeNode('I\Simply\Am\Nonexistent'), $arguments[0]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode('\I\Simply\Am\Nonexistent'), $arguments[0]->getTypeNode()); } /** @@ -440,7 +440,7 @@ public function it_doesnt_fail_to_typehint_nonexistent_RQCN() $classNode = $mirror->reflect(new \ReflectionClass('Fixtures\Prophecy\OptionalDepsClass'), array()); $method = $classNode->getMethod('iHaveAnEvenStrangerTypeHintedArg'); $arguments = $method->getArguments(); - $this->assertEquals(new ArgumentTypeNode('I\Simply\Am\Not'), $arguments[0]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode('\I\Simply\Am\Not'), $arguments[0]->getTypeNode()); } /** @@ -546,7 +546,7 @@ public function it_can_double_a_class_with_union_argument_types() $classNode = (new ClassMirror())->reflect(new \ReflectionClass('Fixtures\Prophecy\UnionArgumentTypes'), []); $methodNode = $classNode->getMethods()['doSomething']; - $this->assertEquals(new ArgumentTypeNode('bool', '\\stdClass'), $methodNode->getArguments()[0]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode('\\stdClass', 'bool'), $methodNode->getArguments()[0]->getTypeNode()); } /** @test */ @@ -569,7 +569,7 @@ public function it_can_double_inherited_self_return_type() $classNode = (new ClassMirror())->reflect(new \ReflectionClass('Fixtures\Prophecy\ClassExtendAbstractWithMethodWithReturnType'), []); $methodNode = $classNode->getMethods()['returnSelf']; - $this->assertEquals(new ReturnTypeNode('Fixtures\Prophecy\AbstractBaseClassWithMethodWithReturnType'), $methodNode->getReturnTypeNode()); + $this->assertEquals(new ReturnTypeNode('\Fixtures\Prophecy\AbstractBaseClassWithMethodWithReturnType'), $methodNode->getReturnTypeNode()); } /** From 05faaded7e0f00c733210e230870ea173c146359 Mon Sep 17 00:00:00 2001 From: kschatzle Date: Tue, 27 Sep 2022 20:21:05 -0500 Subject: [PATCH 3/3] Intersection types in generated code Temp commit. --- .../ClassPatch/TraversablePatchSpec.php | 2 +- .../Generator/ClassCodeGeneratorSpec.php | 44 ++++-- .../Generator/Node/ArgumentTypeNodeSpec.php | 77 ++++++---- .../Generator/Node/ReturnTypeNodeSpec.php | 131 +++++++++-------- .../ClassPatch/ProphecySubjectPatch.php | 3 +- .../Doubler/Generator/ClassCodeGenerator.php | 6 +- .../Doubler/Generator/ClassMirror.php | 133 ++++++++++++------ .../Doubler/Generator/Node/ArgumentNode.php | 2 + .../Generator/Node/ArgumentTypeNode.php | 2 - .../Doubler/Generator/Node/MethodNode.php | 6 +- .../Generator/Node/NameNormalization.php | 2 + .../NameNormalizationAbstract.php | 2 +- .../ReturnTypeNameNormalization.php | 2 +- .../Doubler/Generator/Node/ReturnTypeNode.php | 35 +++-- src/Prophecy/Doubler/Generator/Node/Type.php | 8 ++ .../Node/Type/IntersectionTypeNode.php | 49 +++++++ .../Generator/Node/Type/NamedTypeNode.php | 37 +++++ .../Generator/Node/Type/TypeNodeAbstract.php | 30 ++++ .../Generator/Node/Type/UnionTypeNode.php | 44 ++++++ .../Generator/Node/TypeNodeAbstract.php | 90 ++++++------ tests/Doubler/Generator/ClassMirrorTest.php | 68 +++++---- 21 files changed, 531 insertions(+), 242 deletions(-) create mode 100644 src/Prophecy/Doubler/Generator/Node/Type.php create mode 100644 src/Prophecy/Doubler/Generator/Node/Type/IntersectionTypeNode.php create mode 100644 src/Prophecy/Doubler/Generator/Node/Type/NamedTypeNode.php create mode 100644 src/Prophecy/Doubler/Generator/Node/Type/TypeNodeAbstract.php create mode 100644 src/Prophecy/Doubler/Generator/Node/Type/UnionTypeNode.php diff --git a/spec/Prophecy/Doubler/ClassPatch/TraversablePatchSpec.php b/spec/Prophecy/Doubler/ClassPatch/TraversablePatchSpec.php index 6cafd4cc2..743d01561 100644 --- a/spec/Prophecy/Doubler/ClassPatch/TraversablePatchSpec.php +++ b/spec/Prophecy/Doubler/ClassPatch/TraversablePatchSpec.php @@ -65,7 +65,7 @@ function it_adds_methods_to_implement_iterator(ClassNode $node) $node->addMethod(Argument::that(static function ($value) use ($methodName, $returnType) { return $value instanceof MethodNode && $value->getName() === $methodName - && (\PHP_VERSION_ID < 80100 || $value->getReturnTypeNode()->getTypes() === [$returnType]); + && (\PHP_VERSION_ID < 80100 || $value->getReturnTypeNode()->getType() === [$returnType]); }))->shouldBeCalled(); } diff --git a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php index 8d78e8db2..692443132 100644 --- a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php +++ b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php @@ -10,6 +10,8 @@ use Prophecy\Doubler\Generator\Node\ClassNode; use Prophecy\Doubler\Generator\Node\MethodNode; use Prophecy\Doubler\Generator\Node\ReturnTypeNode; +use Prophecy\Doubler\Generator\Node\Type\NamedTypeNode; +use Prophecy\Doubler\Generator\Node\Type\UnionTypeNode; class ClassCodeGeneratorSpec extends ObjectBehavior { @@ -38,7 +40,9 @@ function it_generates_proper_php_code_for_specific_ClassNode( $method1->returnsReference()->willReturn(false); $method1->isStatic()->willReturn(true); $method1->getArguments()->willReturn(array($argument11, $argument12, $argument13)); - $method1->getReturnTypeNode()->willReturn(new ReturnTypeNode('string', 'null')); + $method1->getReturnTypeNode()->willReturn(new ReturnTypeNode( + new NamedTypeNode('string', true, true) + )); $method1->getCode()->willReturn('return $this->name;'); $method2->getName()->willReturn('getEmail'); @@ -54,7 +58,7 @@ function it_generates_proper_php_code_for_specific_ClassNode( $method3->returnsReference()->willReturn(true); $method3->isStatic()->willReturn(false); $method3->getArguments()->willReturn(array($argument31)); - $method3->getReturnTypeNode()->willReturn(new ReturnTypeNode('string')); + $method3->getReturnTypeNode()->willReturn(new ReturnTypeNode(new NamedTypeNode('string', false, true))); $method3->getCode()->willReturn('return $this->refValue;'); $method4->getName()->willReturn('doSomething'); @@ -62,7 +66,7 @@ function it_generates_proper_php_code_for_specific_ClassNode( $method4->returnsReference()->willReturn(false); $method4->isStatic()->willReturn(false); $method4->getArguments()->willReturn(array()); - $method4->getReturnTypeNode()->willReturn(new ReturnTypeNode('void')); + $method4->getReturnTypeNode()->willReturn(new ReturnTypeNode(new NamedTypeNode('void', false, true))); $method4->getCode()->willReturn('return;'); $method5->getName()->willReturn('returnObject'); @@ -70,7 +74,7 @@ function it_generates_proper_php_code_for_specific_ClassNode( $method5->returnsReference()->willReturn(false); $method5->isStatic()->willReturn(false); $method5->getArguments()->willReturn(array()); - $method5->getReturnTypeNode()->willReturn(new ReturnTypeNode('object')); + $method5->getReturnTypeNode()->willReturn(new ReturnTypeNode(new NamedTypeNode('object', false, true))); $method5->getCode()->willReturn('return;'); $argument11->getName()->willReturn('fullname'); @@ -78,26 +82,26 @@ function it_generates_proper_php_code_for_specific_ClassNode( $argument11->getDefault()->willReturn(null); $argument11->isPassedByReference()->willReturn(false); $argument11->isVariadic()->willReturn(false); - $argument11->getTypeNode()->willReturn(new ArgumentTypeNode('array')); + $argument11->getTypeNode()->willReturn(new ArgumentTypeNode(new NamedTypeNode('array', false, true))); $argument12->getName()->willReturn('class'); $argument12->isOptional()->willReturn(false); $argument12->isPassedByReference()->willReturn(false); $argument12->isVariadic()->willReturn(false); - $argument12->getTypeNode()->willReturn(new ArgumentTypeNode('\ReflectionClass')); + $argument12->getTypeNode()->willReturn(new ArgumentTypeNode(new NamedTypeNode('\ReflectionClass', false, false))); $argument13->getName()->willReturn('instance'); $argument13->isOptional()->willReturn(false); $argument13->isPassedByReference()->willReturn(false); $argument13->isVariadic()->willReturn(false); - $argument13->getTypeNode()->willReturn(new ArgumentTypeNode('object')); + $argument13->getTypeNode()->willReturn(new ArgumentTypeNode(new NamedTypeNode('object', false, true))); $argument21->getName()->willReturn('default'); $argument21->isOptional()->willReturn(true); $argument21->getDefault()->willReturn('ever.zet@gmail.com'); $argument21->isPassedByReference()->willReturn(false); $argument21->isVariadic()->willReturn(false); - $argument21->getTypeNode()->willReturn(new ArgumentTypeNode('string', 'null')); + $argument21->getTypeNode()->willReturn(new ArgumentTypeNode(new NamedTypeNode('string', true, true))); $argument31->getName()->willReturn('refValue'); $argument31->isOptional()->willReturn(false); @@ -206,13 +210,13 @@ function it_generates_proper_php_code_for_variadics( $argument3->isOptional()->willReturn(false); $argument3->isPassedByReference()->willReturn(false); $argument3->isVariadic()->willReturn(true); - $argument3->getTypeNode()->willReturn(new ArgumentTypeNode('\ReflectionClass')); + $argument3->getTypeNode()->willReturn(new ArgumentTypeNode(new NamedTypeNode('\ReflectionClass', false, false))); $argument4->getName()->willReturn('args'); $argument4->isOptional()->willReturn(false); $argument4->isPassedByReference()->willReturn(true); $argument4->isVariadic()->willReturn(true); - $argument4->getTypeNode()->willReturn(new ArgumentTypeNode('\ReflectionClass')); + $argument4->getTypeNode()->willReturn(new ArgumentTypeNode(new NamedTypeNode('\ReflectionClass', false, false))); $code = $this->generate('CustomClass', $class); @@ -263,7 +267,7 @@ function it_overrides_properly_methods_with_args_passed_by_reference( $argument->getDefault()->willReturn(null); $argument->isPassedByReference()->willReturn(true); $argument->isVariadic()->willReturn(false); - $argument->getTypeNode()->willReturn(new ArgumentTypeNode('array')); + $argument->getTypeNode()->willReturn(new ArgumentTypeNode(new NamedTypeNode('array', false, false))); $code = $this->generate('CustomClass', $class); $expected =<<<'PHP' @@ -296,7 +300,14 @@ function it_generates_proper_code_for_union_return_types $method->getVisibility()->willReturn('public'); $method->isStatic()->willReturn(false); $method->getArguments()->willReturn([]); - $method->getReturnTypeNode()->willReturn(new ReturnTypeNode('int', 'string', 'null')); + $method->getReturnTypeNode()->willReturn(new ReturnTypeNode( + new UnionTypeNode( + false, + new NamedTypeNode('int', false, true), + new NamedTypeNode('string', false, true), + new NamedTypeNode('null', false, true) + ) + )); $method->returnsReference()->willReturn(false); $method->getCode()->willReturn(''); @@ -338,7 +349,14 @@ function it_generates_proper_code_for_union_argument_types $method->returnsReference()->willReturn(false); $method->getCode()->willReturn(''); - $argument->getTypeNode()->willReturn(new ArgumentTypeNode('int', 'string', 'null')); + $argument->getTypeNode()->willReturn(new ArgumentTypeNode( + new UnionTypeNode( + false, + new NamedTypeNode('int', false, true), + new NamedTypeNode('string', false, true), + new NamedTypeNode('null', false, true) + ) + )); $argument->getName()->willReturn('arg'); $argument->isPassedByReference()->willReturn(false); $argument->isVariadic()->willReturn(false); diff --git a/spec/Prophecy/Doubler/Generator/Node/ArgumentTypeNodeSpec.php b/spec/Prophecy/Doubler/Generator/Node/ArgumentTypeNodeSpec.php index 1c3696ec9..120e85d5e 100644 --- a/spec/Prophecy/Doubler/Generator/Node/ArgumentTypeNodeSpec.php +++ b/spec/Prophecy/Doubler/Generator/Node/ArgumentTypeNodeSpec.php @@ -4,90 +4,106 @@ use PhpSpec\ObjectBehavior; use Prophecy\Doubler\Generator\Node\ArgumentTypeNode; +use Prophecy\Doubler\Generator\Node\Type\IntersectionTypeNode; +use Prophecy\Doubler\Generator\Node\Type\NamedTypeNode; +use Prophecy\Doubler\Generator\Node\Type\UnionTypeNode; use Prophecy\Exception\Doubler\DoubleException; class ArgumentTypeNodeSpec extends ObjectBehavior { function it_has_no_types_at_start() { - $this->getTypes()->shouldReturn([]); + $this->getType()->shouldReturn(null); } function it_can_have_a_simple_type() { - $this->beConstructedWith('int'); - - $this->getTypes()->shouldReturn(['int']); + $node = new NamedTypeNode('int', false, true); + $this->beConstructedWith($node); + $this->getType()->shouldReturn($node); } - function it_can_have_multiple_types() + function it_can_have_multiple_union_types() { - $this->beConstructedWith('int', 'string'); + $int = new NamedTypeNode('int', false, true); + $string = new NamedTypeNode('string', false, true); + $union = new UnionTypeNode(false, $int, $string); + $this->beConstructedWith($union); - $this->getTypes()->shouldReturn(['int', 'string']); + $this->getType()->shouldReturn($union); } - function it_will_not_prefix_fcqns_that_already_have_prefix() + function it_can_have_multiple_intersection_types() { - $this->beConstructedWith('\\Foo'); + $int = new NamedTypeNode('int', false, true); + $string = new NamedTypeNode('string', false, true); + $intersection = new IntersectionTypeNode(false, $int, $string); + $this->beConstructedWith($intersection); - $this->getTypes()->shouldReturn(['\\Foo']); + $this->getType()->shouldReturn($intersection); } - function it_can_use_shorthand_null_syntax_if_it_has_single_type_plus_null() + function it_can_use_shorthand_null_syntax_if_it_is_named_type_node_and_allows_null() { - $this->beConstructedWith('int', 'null'); + $int = new NamedTypeNode('int', true, true); + $this->beConstructedWith($int); $this->canUseNullShorthand()->shouldReturn(true); } - function it_can_not_use_shorthand_null_syntax_if_it_does_not_allow_null() + function it_can_not_use_shorthand_if_its_not_named_type_node() { - $this->beConstructedWith('int'); + $int = new NamedTypeNode('int', false, true); + $string = new NamedTypeNode('string', false, true); + $intersection = new IntersectionTypeNode(false, $int, $string); + $this->beConstructedWith($intersection); $this->canUseNullShorthand()->shouldReturn(false); } - function it_can_not_use_shorthand_null_syntax_if_it_has_more_than_one_non_null_type() + function it_can_not_use_shorthand_if_its_named_type_node_but_does_not_allow_null() { - $this->beConstructedWith('int', 'string', 'null'); + $int = new NamedTypeNode('int', false, true); + $this->beConstructedWith($int); $this->canUseNullShorthand()->shouldReturn(false); } - function it_can_return_non_null_types() - { - $this->beConstructedWith('int', 'null'); - - $this->getNonNullTypes()->shouldReturn(['int']); - } - function it_does_not_allow_standalone_null() { - $this->beConstructedWith('null'); + $null = new NamedTypeNode('null', false, true); + $this->beConstructedWith($null); $this->shouldThrow(DoubleException::class)->duringInstantiation(); } function it_does_not_allow_union_mixed() { - $this->beConstructedWith('mixed', 'int'); + $void = new NamedTypeNode('mixed', false, true); + $int = new NamedTypeNode('int', false, true); + $union = new UnionTypeNode(false, $void, $int); + + $this->beConstructedWith($union); if (PHP_VERSION_ID >=80000) { $this->shouldThrow(DoubleException::class)->duringInstantiation(); } } - function it_does_not_prefix_false() + function it_does_not_prefix_false_in_a_union() { - $this->beConstructedWith('false', 'array'); + $array = new NamedTypeNode('array', false, true); + $false = new NamedTypeNode('false', false, true); + $union = new UnionTypeNode(false, $array, $false); + $this->beConstructedWith($union); - $this->getTypes()->shouldReturn(['false', 'array']); + $this->getType()->getTypes()[0]->getName()->shouldReturn('array'); } function it_does_not_allow_standalone_false() { - $this->beConstructedWith('false'); + $false = new NamedTypeNode('false', false, true); + $this->beConstructedWith($false); if (PHP_VERSION_ID >=80000) { $this->shouldThrow(DoubleException::class)->duringInstantiation(); @@ -96,7 +112,8 @@ function it_does_not_allow_standalone_false() function it_does_not_allow_nullable_false() { - $this->beConstructedWith('null', 'false'); + $false = new NamedTypeNode('false', true, true); + $this->beConstructedWith($false); if (PHP_VERSION_ID >=80000) { $this->shouldThrow(DoubleException::class)->duringInstantiation(); diff --git a/spec/Prophecy/Doubler/Generator/Node/ReturnTypeNodeSpec.php b/spec/Prophecy/Doubler/Generator/Node/ReturnTypeNodeSpec.php index d93770490..d32219206 100644 --- a/spec/Prophecy/Doubler/Generator/Node/ReturnTypeNodeSpec.php +++ b/spec/Prophecy/Doubler/Generator/Node/ReturnTypeNodeSpec.php @@ -2,119 +2,117 @@ namespace spec\Prophecy\Doubler\Generator\Node; +use Fixtures\Prophecy\UnionReturnTypes; use PhpSpec\ObjectBehavior; +use Prophecy\Doubler\Generator\Node\Type\IntersectionTypeNode; +use Prophecy\Doubler\Generator\Node\Type\NamedTypeNode; +use Prophecy\Doubler\Generator\Node\Type\UnionTypeNode; use Prophecy\Exception\Doubler\DoubleException; class ReturnTypeNodeSpec extends ObjectBehavior { function it_has_no_return_types_at_start() { - $this->getTypes()->shouldReturn([]); + $this->getType()->shouldReturn(null); } - function it_can_have_a_simple_type() - { - $this->beConstructedWith('int'); - - $this->getTypes()->shouldReturn(['int']); - } - - function it_can_have_multiple_types() - { - $this->beConstructedWith('int', 'string'); - - $this->getTypes()->shouldReturn(['int', 'string']); + function it_can_have_a_simple_type() { + $node = new NamedTypeNode('int', false, true); + $this->beConstructedWith($node); + $this->getType()->shouldReturn($node); } - function it_can_have_void_type() - { - $this->beConstructedWith('void'); - - $this->getTypes()->shouldReturn(['void']); - } - - function it_will_not_normalise_type_aliases_types() + function it_can_have_multiple_union_types() { - $this->beConstructedWith('double', 'real', 'boolean', 'integer'); + $int = new NamedTypeNode('int', false, true); + $string = new NamedTypeNode('string', false, true); + $union = new UnionTypeNode(false, $int, $string); + $this->beConstructedWith($union); - $this->getTypes()->shouldNotBe(['float', 'bool', 'int']); + $this->getType()->shouldReturn($union); } - function it_will_not_prefix_fcqns() + function it_can_have_multiple_intersection_types() { - $this->beConstructedWith('Foo'); + $int = new NamedTypeNode('int', false, true); + $string = new NamedTypeNode('string', false, true); + $intersection = new IntersectionTypeNode(false, $int, $string); + $this->beConstructedWith($intersection); - $this->getTypes()->shouldNotBe(['\\Foo']); + $this->getType()->shouldReturn($intersection); } - function it_will_not_prefix_fcqns_that_already_have_prefix() + function it_can_have_void_type() { - $this->beConstructedWith('\\Foo'); + $void = new NamedTypeNode('void', false, true); + $this->beConstructedWith($void); - $this->getTypes()->shouldReturn(['\\Foo']); + $this->getType()->shouldReturn($void); } - function it_can_use_shorthand_null_syntax_if_it_has_single_type_plus_null() + function it_can_use_shorthand_null_syntax_if_it_is_named_type_node_and_allows_null() { - $this->beConstructedWith('int', 'null'); + $int = new NamedTypeNode('int', true, true); + $this->beConstructedWith($int); $this->canUseNullShorthand()->shouldReturn(true); } - function it_can_not_use_shorthand_null_syntax_if_it_does_not_allow_null() + function it_can_not_use_shorthand_if_its_not_named_type_node() { - $this->beConstructedWith('int'); + $int = new NamedTypeNode('int', false, true); + $string = new NamedTypeNode('string', false, true); + $intersection = new IntersectionTypeNode(false, $int, $string); + $this->beConstructedWith($intersection); $this->canUseNullShorthand()->shouldReturn(false); } - function it_can_not_use_shorthand_null_syntax_if_it_has_more_than_one_non_null_type() + function it_can_not_use_shorthand_if_its_named_type_node_but_does_not_allow_null() { - $this->beConstructedWith('int', 'string', 'null'); + $int = new NamedTypeNode('int', false, true); + $this->beConstructedWith($int); $this->canUseNullShorthand()->shouldReturn(false); } - function it_can_return_non_null_types() - { - $this->beConstructedWith('int', 'null'); - - $this->getNonNullTypes()->shouldReturn(['int']); - } - - function it_does_not_allow_standalone_null() - { - $this->beConstructedWith('null'); - - $this->shouldThrow(DoubleException::class)->duringInstantiation(); - } - function it_does_not_allow_union_void() { - $this->beConstructedWith('void', 'int'); + $void = new NamedTypeNode('void', false, true); + $int = new NamedTypeNode('int', false, true); + $union = new UnionTypeNode(false, $void, $int); + $this->beConstructedWith($union); $this->shouldThrow(DoubleException::class)->duringInstantiation(); } function it_does_not_allow_union_mixed() { - $this->beConstructedWith('mixed', 'int'); + $void = new NamedTypeNode('mixed', false, true); + $int = new NamedTypeNode('int', false, true); + $union = new UnionTypeNode(false, $void, $int); + + $this->beConstructedWith($union); if (PHP_VERSION_ID >=80000) { $this->shouldThrow(DoubleException::class)->duringInstantiation(); } } - function it_does_not_prefix_false() + function it_does_not_prefix_false_in_a_union() { - $this->beConstructedWith('false', 'array'); + $array = new NamedTypeNode('array', false, true); + $false = new NamedTypeNode('false', false, true); + $union = new UnionTypeNode(false, $array, $false); + $this->beConstructedWith($union); - $this->getTypes()->shouldReturn(['false', 'array']); + $this->getType()->getTypes()[0]->getName()->shouldReturn('array'); } function it_does_not_allow_standalone_false() { - $this->beConstructedWith('false'); + $false = new NamedTypeNode('false', false, true); + $this->beConstructedWith($false); if (PHP_VERSION_ID >=80000) { $this->shouldThrow(DoubleException::class)->duringInstantiation(); @@ -123,7 +121,8 @@ function it_does_not_allow_standalone_false() function it_does_not_allow_nullable_false() { - $this->beConstructedWith('null', 'false'); + $false = new NamedTypeNode('false', true, true); + $this->beConstructedWith($false); if (PHP_VERSION_ID >=80000) { $this->shouldThrow(DoubleException::class)->duringInstantiation(); @@ -132,35 +131,43 @@ function it_does_not_allow_nullable_false() function it_does_not_prefix_never() { - $this->beConstructedWith('never'); + $never = new NamedTypeNode('never', false, true); + $this->beConstructedWith($never); - $this->getTypes()->shouldReturn(['never']); + $this->getType()->getName()->shouldBe('never'); } function it_does_not_allow_union_never() { - $this->beConstructedWith('never', 'int'); + $never = new NamedTypeNode('never', false, true); + $int = new NamedTypeNode('int', false, true); + $union = new UnionTypeNode(false, $never, $int); + + $this->beConstructedWith($union); $this->shouldThrow(DoubleException::class)->duringInstantiation(); } function it_has_a_return_statement_if_it_is_a_simple_type() { - $this->beConstructedWith('int'); + $int = new NamedTypeNode('int', false, true); + $this->beConstructedWith($int); $this->shouldHaveReturnStatement(); } function it_does_not_have_return_statement_if_it_returns_void() { - $this->beConstructedWith('void'); + $void = new NamedTypeNode('void', false, true); + $this->beConstructedWith($void); $this->shouldNotHaveReturnStatement(); } function it_does_not_have_return_statement_if_it_returns_never() { - $this->beConstructedWith('never'); + $never = new NamedTypeNode('never', false, true); + $this->beConstructedWith($never); $this->shouldNotHaveReturnStatement(); } diff --git a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php index 92394c73c..01f7da105 100644 --- a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php +++ b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php @@ -16,6 +16,7 @@ use Prophecy\Doubler\Generator\Node\MethodNode; use Prophecy\Doubler\Generator\Node\ArgumentNode; use Prophecy\Doubler\Generator\Node\ReturnTypeNode; +use Prophecy\Doubler\Generator\Node\Type\NamedTypeNode; /** * Add Prophecy functionality to the double. @@ -65,7 +66,7 @@ public function apply(ClassNode $node) $prophecySetter = new MethodNode('setProphecy'); $prophecyArgument = new ArgumentNode('prophecy'); - $prophecyArgument->setTypeNode(new ArgumentTypeNode('\Prophecy\Prophecy\ProphecyInterface')); + $prophecyArgument->setTypeNode(new ArgumentTypeNode(new NamedTypeNode('\Prophecy\Prophecy\ProphecyInterface'))); $prophecySetter->addArgument($prophecyArgument); $prophecySetter->setCode(<<objectProphecyClosure) { diff --git a/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php b/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php index 52e5e0455..94b7dc784 100644 --- a/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php +++ b/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php @@ -76,15 +76,15 @@ private function generateMethod(Node\MethodNode $method) private function generateTypes(TypeNodeAbstract $typeNode): string { - if (!$typeNode->getTypes()) { + if (!$typeNode->getType()) { return ''; } // When we require PHP 8 we can stop generating ?foo nullables and remove this first block if ($typeNode->canUseNullShorthand()) { - return sprintf( '?%s', $typeNode->getNonNullTypes()[0]); + return sprintf( '?%s', $typeNode->getType()->getName()); } else { - return join('|', $typeNode->getTypes()); + return (string) $typeNode->getType(); } } diff --git a/src/Prophecy/Doubler/Generator/ClassMirror.php b/src/Prophecy/Doubler/Generator/ClassMirror.php index 55c174940..db4e1a6ee 100644 --- a/src/Prophecy/Doubler/Generator/ClassMirror.php +++ b/src/Prophecy/Doubler/Generator/ClassMirror.php @@ -12,9 +12,13 @@ namespace Prophecy\Doubler\Generator; use Prophecy\Doubler\Generator\Node\ArgumentTypeNode; +use Prophecy\Doubler\Generator\Node\NameNormalization; use Prophecy\Doubler\Generator\Node\NameNormalization\ArgumentTypeNameNormalization; use Prophecy\Doubler\Generator\Node\NameNormalization\ReturnTypeNameNormalization; use Prophecy\Doubler\Generator\Node\ReturnTypeNode; +use Prophecy\Doubler\Generator\Node\Type\IntersectionTypeNode; +use Prophecy\Doubler\Generator\Node\Type\NamedTypeNode; +use Prophecy\Doubler\Generator\Node\Type\UnionTypeNode; use Prophecy\Exception\InvalidArgumentException; use Prophecy\Exception\Doubler\ClassMirrorException; use ReflectionClass; @@ -151,20 +155,14 @@ private function reflectMethodToNode(ReflectionMethod $method, Node\ClassNode $c } if ($method->hasReturnType()) { - $returnTypes = $this->getTypeHints($method->getReturnType(), $method->getDeclaringClass(), $method->getReturnType()->allowsNull()); - - $normalization = new ReturnTypeNameNormalization(); - $returnTypes = $normalization->normalize(...$returnTypes); - - $node->setReturnTypeNode(new ReturnTypeNode(...$returnTypes)); + $returnTypes = $this->getTypeDeclarations(new ReturnTypeNameNormalization(), $method->getDeclaringClass(), $method->getReturnType()); + $returnTypes = array_shift($returnTypes); + $node->setReturnTypeNode(new ReturnTypeNode($returnTypes)); } elseif (method_exists($method, 'hasTentativeReturnType') && $method->hasTentativeReturnType()) { - $returnTypes = $this->getTypeHints($method->getTentativeReturnType(), $method->getDeclaringClass(), $method->getTentativeReturnType()->allowsNull()); - - $normalization = new ReturnTypeNameNormalization(); - $returnTypes = $normalization->normalize(...$returnTypes); - - $node->setReturnTypeNode(new ReturnTypeNode(...$returnTypes)); + $returnTypes = $this->getTypeDeclarations(new ReturnTypeNameNormalization(), $method->getDeclaringClass(), $method->getTentativeReturnType()); + $returnTypes = array_shift($returnTypes); + $node->setReturnTypeNode(new ReturnTypeNode($returnTypes)); } if (is_array($params = $method->getParameters()) && count($params)) { @@ -182,12 +180,10 @@ private function reflectArgumentToNode(ReflectionParameter $parameter, Node\Meth $node = new Node\ArgumentNode($name); if ($parameter->hasType()) { - $typeHints = $this->getTypeHints($parameter->getType(), $parameter->getDeclaringClass(), $parameter->allowsNull()); + $typeHints = $this->getTypeDeclarations(new ArgumentTypeNameNormalization(), $parameter->getDeclaringClass(), $parameter->getType()); + $typeHints = array_shift($typeHints); - $normalization = new ArgumentTypeNameNormalization(); - $typeHints = $normalization->normalize(...$typeHints); - - $node->setTypeNode(new ArgumentTypeNode(...$typeHints)); + $node->setTypeNode(new ArgumentTypeNode($typeHints)); } if ($parameter->isVariadic()) { @@ -228,42 +224,89 @@ private function getDefaultValue(ReflectionParameter $parameter) return $parameter->getDefaultValue(); } - private function getTypeHints(?ReflectionType $type, ?ReflectionClass $class, bool $allowsNull) : array + private function getTypeDeclarations(NameNormalization $normalization, ReflectionClass $class = null, ReflectionType ...$types): array { - $types = []; + $nodes = []; + foreach ($types as $type) { + if ($type instanceof ReflectionUnionType) { + $nodes[] = new UnionTypeNode( + $type->allowsNull(), + ...$this->getTypeDeclarations( + $normalization, + $class, + ...$type->getTypes() + ) + ); + } - if ($type instanceof ReflectionNamedType) { - $types = [$type->getName()]; + if ($type instanceof ReflectionIntersectionType) { + $nodes[] = new IntersectionTypeNode( + $type->allowsNull(), + ...$this->getTypeDeclarations( + $normalization, + $class, + ...$type->getTypes() + ) + ); + } - } - elseif ($type instanceof ReflectionUnionType) { - $types = $type->getTypes(); - } - elseif ($type instanceof ReflectionIntersectionType) { - throw new ClassMirrorException('Doubling intersection types is not supported', $class); - } - elseif(is_object($type)) { - throw new ClassMirrorException('Unknown reflection type ' . get_class($type), $class); - } + if ($type instanceof ReflectionNamedType) { + $name = $type->getName(); - $types = array_map( - function(string $type) use ($class) { - if ($type === 'self') { - return $class->getName(); + if ($name === 'self') { + $name = $class->getName(); } - if ($type === 'parent') { - return $class->getParentClass()->getName(); + elseif ($name === 'parent') { + $name = $class->getParentClass()->getName(); } - return $type; - }, - $types - ); - - if ($types && $types != ['mixed'] && $allowsNull) { - $types[] = 'null'; + $nodes[] = new NamedTypeNode( + $normalization->getRealType($name), + $type->allowsNull(), + $type->isBuiltin() + ); + } } - return $types; + return $nodes; } + +// private function getTypeHints(?ReflectionType $type, ?ReflectionClass $class, bool $allowsNull) : array +// { +// $types = []; +// +// if ($type instanceof ReflectionNamedType) { +// $types = [$type->getName()]; +// +// } +// elseif ($type instanceof ReflectionUnionType) { +// $types = $type->getTypes(); +// } +// elseif ($type instanceof ReflectionIntersectionType) { +// throw new ClassMirrorException('Doubling intersection types is not supported', $class); +// } +// elseif(is_object($type)) { +// throw new ClassMirrorException('Unknown reflection type ' . get_class($type), $class); +// } +// +// $types = array_map( +// function(string $type) use ($class) { +// if ($type === 'self') { +// return $class->getName(); +// } +// if ($type === 'parent') { +// return $class->getParentClass()->getName(); +// } +// +// return $type; +// }, +// $types +// ); +// +// if ($types && $types != ['mixed'] && $allowsNull) { +// $types[] = 'null'; +// } +// +// return $types; +// } } diff --git a/src/Prophecy/Doubler/Generator/Node/ArgumentNode.php b/src/Prophecy/Doubler/Generator/Node/ArgumentNode.php index da7fed4e1..22837673f 100644 --- a/src/Prophecy/Doubler/Generator/Node/ArgumentNode.php +++ b/src/Prophecy/Doubler/Generator/Node/ArgumentNode.php @@ -98,6 +98,7 @@ public function isVariadic() */ public function getTypeHint() { + // @TODO fix. $type = $this->typeNode->getNonNullTypes() ? $this->typeNode->getNonNullTypes()[0] : null; return $type ? ltrim($type, '\\') : null; @@ -127,6 +128,7 @@ public function isNullable() */ public function setAsNullable($isNullable = true) { + // @TOOD fix $nonNullTypes = $this->typeNode->getNonNullTypes(); $this->typeNode = $isNullable ? new ArgumentTypeNode('null', ...$nonNullTypes) : new ArgumentTypeNode(...$nonNullTypes); } diff --git a/src/Prophecy/Doubler/Generator/Node/ArgumentTypeNode.php b/src/Prophecy/Doubler/Generator/Node/ArgumentTypeNode.php index 0a18b91e1..b322adad3 100644 --- a/src/Prophecy/Doubler/Generator/Node/ArgumentTypeNode.php +++ b/src/Prophecy/Doubler/Generator/Node/ArgumentTypeNode.php @@ -2,8 +2,6 @@ namespace Prophecy\Doubler\Generator\Node; -use Prophecy\Exception\Doubler\DoubleException; - class ArgumentTypeNode extends TypeNodeAbstract { diff --git a/src/Prophecy/Doubler/Generator/Node/MethodNode.php b/src/Prophecy/Doubler/Generator/Node/MethodNode.php index ece652f9f..bf5cb9587 100644 --- a/src/Prophecy/Doubler/Generator/Node/MethodNode.php +++ b/src/Prophecy/Doubler/Generator/Node/MethodNode.php @@ -111,6 +111,7 @@ public function getArguments() */ public function hasReturnType() { + // @TODO fix. return (bool) $this->returnTypeNode->getNonNullTypes(); } @@ -134,10 +135,12 @@ public function setReturnType($type = null) */ public function setNullableReturnType($bool = true) { + // @TOOD fix if ($bool) { - $this->returnTypeNode = new ReturnTypeNode('null', ...$this->returnTypeNode->getTypes()); + $this->returnTypeNode = new ReturnTypeNode('null', ...$this->returnTypeNode->getType()); } else { + // @TODO fix. $this->returnTypeNode = new ReturnTypeNode(...$this->returnTypeNode->getNonNullTypes()); } } @@ -148,6 +151,7 @@ public function setNullableReturnType($bool = true) */ public function getReturnType() { + // @TODO fix. if ($types = $this->returnTypeNode->getNonNullTypes()) { return $types[0]; diff --git a/src/Prophecy/Doubler/Generator/Node/NameNormalization.php b/src/Prophecy/Doubler/Generator/Node/NameNormalization.php index 9188eb19d..cefb49001 100644 --- a/src/Prophecy/Doubler/Generator/Node/NameNormalization.php +++ b/src/Prophecy/Doubler/Generator/Node/NameNormalization.php @@ -5,4 +5,6 @@ interface NameNormalization { public function normalize(string ...$types): array; + + public function getRealType(string $type): string; } \ No newline at end of file diff --git a/src/Prophecy/Doubler/Generator/Node/NameNormalization/NameNormalizationAbstract.php b/src/Prophecy/Doubler/Generator/Node/NameNormalization/NameNormalizationAbstract.php index b201433d0..138de7d1f 100644 --- a/src/Prophecy/Doubler/Generator/Node/NameNormalization/NameNormalizationAbstract.php +++ b/src/Prophecy/Doubler/Generator/Node/NameNormalization/NameNormalizationAbstract.php @@ -17,7 +17,7 @@ public function normalize(string ...$types): array return array_values($normalizedTypes); } - protected function getRealType(string $type): string + public function getRealType(string $type): string { switch ($type) { // type aliases diff --git a/src/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalization.php b/src/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalization.php index bbf9c42ab..31bcc2212 100644 --- a/src/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalization.php +++ b/src/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalization.php @@ -4,7 +4,7 @@ class ReturnTypeNameNormalization extends NameNormalizationAbstract { - protected function getRealType(string $type): string + public function getRealType(string $type): string { switch ($type) { case 'void': diff --git a/src/Prophecy/Doubler/Generator/Node/ReturnTypeNode.php b/src/Prophecy/Doubler/Generator/Node/ReturnTypeNode.php index 893eed699..274627c2d 100644 --- a/src/Prophecy/Doubler/Generator/Node/ReturnTypeNode.php +++ b/src/Prophecy/Doubler/Generator/Node/ReturnTypeNode.php @@ -2,17 +2,26 @@ namespace Prophecy\Doubler\Generator\Node; +use Prophecy\Doubler\Generator\Node\Type\IntersectionTypeNode; +use Prophecy\Doubler\Generator\Node\Type\NamedTypeNode; +use Prophecy\Doubler\Generator\Node\Type\UnionTypeNode; use Prophecy\Exception\Doubler\DoubleException; final class ReturnTypeNode extends TypeNodeAbstract { protected function guardIsValidType() { - if (in_array('void', $this->types) && count($this->types) !== 1) { - throw new DoubleException('void cannot be part of a union'); - } - if (in_array('never', $this->types) && count($this->types) !== 1) { - throw new DoubleException('never cannot be part of a union'); + if ($this->type instanceof UnionTypeNode) { + /** @var NamedTypeNode $type */ + foreach ($this->type->getTypes() as $type) { + if ($type->getName() === 'void') { + throw new DoubleException('void cannot be part of a union'); + } + elseif ($type->getName() === 'never') + { + throw new DoubleException('never cannot be part of a union'); + } + } } parent::guardIsValidType(); @@ -23,12 +32,22 @@ protected function guardIsValidType() */ public function isVoid() { - return $this->types == ['void']; + return $this->type instanceof NamedTypeNode + && $this->type->getName() == 'void'; } public function hasReturnStatement(): bool { - return $this->types !== ['void'] - && $this->types !== ['never']; + if (!$this->type instanceof Type) { + return false; + } + + if ($this->type instanceof NamedTypeNode + && in_array($this->type->getName(), ['void', 'never'])) + { + return false; + } + + return true; } } diff --git a/src/Prophecy/Doubler/Generator/Node/Type.php b/src/Prophecy/Doubler/Generator/Node/Type.php new file mode 100644 index 000000000..487f5ac98 --- /dev/null +++ b/src/Prophecy/Doubler/Generator/Node/Type.php @@ -0,0 +1,8 @@ +allowsNull = $allowsNulls; + $this->types = $types; + } + + /** + * @return NamedTypeNode[] + */ + public function getTypes(): array + { + return $this->types; + } + + public function hasReturnStatement(): bool + { + return true; + } + + public function __toString(): string + { + $string = ''; + foreach ($this->types as $type) { + $string .= $type . '&'; + } + return '(' . rtrim($string, '&') . ')'; + } +} \ No newline at end of file diff --git a/src/Prophecy/Doubler/Generator/Node/Type/NamedTypeNode.php b/src/Prophecy/Doubler/Generator/Node/Type/NamedTypeNode.php new file mode 100644 index 000000000..ca4a68b3d --- /dev/null +++ b/src/Prophecy/Doubler/Generator/Node/Type/NamedTypeNode.php @@ -0,0 +1,37 @@ +name = $name; + $this->isBuiltIn = $isBuiltIn; + $this->allowsNull = $allowsNull; + } + + public function getName(): string + { + return $this->name; + } + + public function isBuiltIn(): bool + { + return $this->isBuiltIn; + } + + public function __toString(): string + { + return $this->name; + } +} \ No newline at end of file diff --git a/src/Prophecy/Doubler/Generator/Node/Type/TypeNodeAbstract.php b/src/Prophecy/Doubler/Generator/Node/Type/TypeNodeAbstract.php new file mode 100644 index 000000000..c90664711 --- /dev/null +++ b/src/Prophecy/Doubler/Generator/Node/Type/TypeNodeAbstract.php @@ -0,0 +1,30 @@ +allowsNull; + } + + public function canUseNullShorthand(): bool + { + return $this->allowsNull; + } + + public function getTypes(): array + { + return []; + } + + public function getNonNullTypes(): array + { + } +} \ No newline at end of file diff --git a/src/Prophecy/Doubler/Generator/Node/Type/UnionTypeNode.php b/src/Prophecy/Doubler/Generator/Node/Type/UnionTypeNode.php new file mode 100644 index 000000000..ae027a8df --- /dev/null +++ b/src/Prophecy/Doubler/Generator/Node/Type/UnionTypeNode.php @@ -0,0 +1,44 @@ +allowsNull = $allowsNull; + $this->types = $types; + } + + /** + * @return NamedTypeNode[] + */ + public function getTypes(): array + { + return $this->types; + } + + public function __toString(): string + { + $string = ''; + foreach ($this->types as $type) { + $string .= $type . '|'; + } + return rtrim($string, '|'); + } +} \ No newline at end of file diff --git a/src/Prophecy/Doubler/Generator/Node/TypeNodeAbstract.php b/src/Prophecy/Doubler/Generator/Node/TypeNodeAbstract.php index f0e5cc597..4153a5771 100644 --- a/src/Prophecy/Doubler/Generator/Node/TypeNodeAbstract.php +++ b/src/Prophecy/Doubler/Generator/Node/TypeNodeAbstract.php @@ -2,76 +2,66 @@ namespace Prophecy\Doubler\Generator\Node; +use Prophecy\Doubler\Generator\Node\Type\IntersectionTypeNode; +use Prophecy\Doubler\Generator\Node\Type\NamedTypeNode; +use Prophecy\Doubler\Generator\Node\Type\UnionTypeNode; use Prophecy\Exception\Doubler\DoubleException; abstract class TypeNodeAbstract { - /** @var string[] */ - protected $types = []; + /** @var ?Type $type */ + protected $type; - public function __construct(string ...$types) + public function __construct(?Type $type = null) { - $this->types = $types; + $this->type = $type; $this->guardIsValidType(); } public function canUseNullShorthand(): bool { - return in_array('null', $this->types) && count($this->types) <= 2; + return $this->type instanceof NamedTypeNode + && $this->type->getName() !== 'mixed' + && $this->type->allowsNull(); } - public function getTypes(): array + public function getType(): ?Type { - return array_values($this->types); - } - - public function getNonNullTypes(): array - { - $nonNullTypes = $this->types; - - if (($key = array_search('null', $nonNullTypes)) !== false) { - unset($nonNullTypes[$key]); - } - - return array_values($nonNullTypes); - } - - /** - * Order of array does not matter. $array has to be non empty. - * - * @param $array - * @return bool - */ - protected function doesArrayEqual($array) - { - if (empty($this->types)) { - return false; - } - $intersection = array_intersect($this->types, $array); - - return count($intersection) == count($this->types); + return $this->type; } protected function guardIsValidType() { - if(!empty($this->types) && count(array_intersect($this->types, ['false', 'null'])) == count($this->types)){ - throw new DoubleException('Type cannot be nullable false'); - } - - if ($this->doesArrayEqual(['null'])) { - throw new DoubleException('Type cannot be standalone null'); + if ($this->type instanceof UnionTypeNode) { + /** @var NamedTypeNode $type */ + foreach ($this->type->getTypes() as $type) { + if (\PHP_VERSION_ID >= 80000 && $type->getName() === 'mixed') { + throw new DoubleException('mixed cannot be part of a union'); + } + } } - - if ($this->doesArrayEqual(['false'])) { - throw new DoubleException('Type cannot be standalone false'); + elseif($this->type instanceof IntersectionTypeNode) + { + /** @var NamedTypeNode $type */ + foreach ($this->type->getTypes() as $type) { + if (\PHP_VERSION_ID >= 80000 && $type->getName() === 'mixed') { + throw new DoubleException('mixed cannot be part of an intersection'); + } + } } - - if ($this->doesArrayEqual(['false', 'null'])) { - throw new DoubleException('Type cannot be nullable false'); - } - - if (\PHP_VERSION_ID >= 80000 && in_array('mixed', $this->types) && count($this->types) !== 1) { - throw new DoubleException('mixed cannot be part of a union'); + elseif($this->type instanceof NamedTypeNode) + { + if ($this->type->getName() === 'null') { + throw new DoubleException('Type cannot be standalone null'); + } + + if ($this->type->getName() === 'false' && $this->type->allowsNull()) { + throw new DoubleException('Type cannot be nullable false'); + } + + if ($this->type->getName() === 'false') { + throw new DoubleException('Type cannot be standalone false'); + } } } } diff --git a/tests/Doubler/Generator/ClassMirrorTest.php b/tests/Doubler/Generator/ClassMirrorTest.php index c2ab9d944..94f758356 100644 --- a/tests/Doubler/Generator/ClassMirrorTest.php +++ b/tests/Doubler/Generator/ClassMirrorTest.php @@ -6,6 +6,9 @@ use Prophecy\Doubler\Generator\ClassMirror; use Prophecy\Doubler\Generator\Node\ArgumentTypeNode; use Prophecy\Doubler\Generator\Node\ReturnTypeNode; +use Prophecy\Doubler\Generator\Node\Type\IntersectionTypeNode; +use Prophecy\Doubler\Generator\Node\Type\NamedTypeNode; +use Prophecy\Doubler\Generator\Node\Type\UnionTypeNode; use Prophecy\Exception\Doubler\ClassMirrorException; use Prophecy\Exception\InvalidArgumentException; @@ -103,18 +106,18 @@ public function it_properly_reads_methods_arguments_with_types() $this->assertEquals('arg_1', $argNodes[0]->getName()); - $this->assertEquals(new ArgumentTypeNode('\ArrayAccess'), $argNodes[0]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode(new NamedTypeNode('\ArrayAccess')), $argNodes[0]->getTypeNode()); $this->assertFalse($argNodes[0]->isOptional()); $this->assertEquals('arg_2', $argNodes[1]->getName()); - $this->assertEquals(new ArgumentTypeNode('array'), $argNodes[1]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode(new NamedTypeNode('array', false, true)), $argNodes[1]->getTypeNode()); $this->assertTrue($argNodes[1]->isOptional()); $this->assertEquals(array(), $argNodes[1]->getDefault()); $this->assertFalse($argNodes[1]->isPassedByReference()); $this->assertFalse($argNodes[1]->isVariadic()); $this->assertEquals('arg_3', $argNodes[2]->getName()); - $this->assertEquals(new ArgumentTypeNode('\ArrayAccess', 'null'), $argNodes[2]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode(new NamedTypeNode('\ArrayAccess', true, false)), $argNodes[2]->getTypeNode()); $this->assertTrue($argNodes[2]->isOptional()); $this->assertNull($argNodes[2]->getDefault()); $this->assertFalse($argNodes[2]->isPassedByReference()); @@ -138,13 +141,13 @@ public function it_properly_reads_methods_arguments_with_callable_types() $this->assertCount(2, $argNodes); $this->assertEquals('arg_1', $argNodes[0]->getName()); - $this->assertEquals(new ArgumentTypeNode('callable'), $argNodes[0]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode(new NamedTypeNode('callable', false, true)), $argNodes[0]->getTypeNode()); $this->assertFalse($argNodes[0]->isOptional()); $this->assertFalse($argNodes[0]->isPassedByReference()); $this->assertFalse($argNodes[0]->isVariadic()); $this->assertEquals('arg_2', $argNodes[1]->getName()); - $this->assertEquals(new ArgumentTypeNode('callable', 'null'), $argNodes[1]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode(new NamedTypeNode('callable', true, true)), $argNodes[1]->getTypeNode()); $this->assertTrue($argNodes[1]->isOptional()); $this->assertNull($argNodes[1]->getDefault()); $this->assertFalse($argNodes[1]->isPassedByReference()); @@ -195,7 +198,7 @@ public function it_properly_reads_methods_typehinted_variadic_arguments() $this->assertCount(1, $argNodes); $this->assertEquals('args', $argNodes[0]->getName()); - $this->assertEquals(new ArgumentTypeNode('array'), $argNodes[0]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode(new NamedTypeNode('array', false, true)), $argNodes[0]->getTypeNode()); $this->assertFalse($argNodes[0]->isOptional()); $this->assertFalse($argNodes[0]->isPassedByReference()); $this->assertTrue($argNodes[0]->isVariadic()); @@ -352,9 +355,9 @@ public function it_reflects_return_typehints() $this->assertTrue($classNode->hasMethod('getSelf')); $this->assertTrue($classNode->hasMethod('getParent')); - $this->assertEquals(new ReturnTypeNode('string'), $classNode->getMethod('getName')->getReturnTypeNode()); - $this->assertEquals(new ReturnTypeNode('\Fixtures\Prophecy\WithReturnTypehints'), $classNode->getMethod('getSelf')->getReturnTypeNode()); - $this->assertEquals(new ReturnTypeNode('\Fixtures\Prophecy\EmptyClass'), $classNode->getMethod('getParent')->getReturnTypeNode()); + $this->assertEquals(new ReturnTypeNode(new NamedTypeNode('string', false, true)), $classNode->getMethod('getName')->getReturnTypeNode()); + $this->assertEquals(new ReturnTypeNode(new NamedTypeNode('\Fixtures\Prophecy\WithReturnTypehints')), $classNode->getMethod('getSelf')->getReturnTypeNode()); + $this->assertEquals(new ReturnTypeNode(new NamedTypeNode('\Fixtures\Prophecy\EmptyClass')), $classNode->getMethod('getParent')->getReturnTypeNode()); } /** @@ -413,7 +416,7 @@ public function it_doesnt_fail_to_typehint_nonexistent_FQCN() $classNode = $mirror->reflect(new \ReflectionClass('Fixtures\Prophecy\OptionalDepsClass'), array()); $method = $classNode->getMethod('iHaveAStrangeTypeHintedArg'); $arguments = $method->getArguments(); - $this->assertEquals(new ArgumentTypeNode('\I\Simply\Am\Nonexistent'), $arguments[0]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode(new NamedTypeNode('\I\Simply\Am\Nonexistent')), $arguments[0]->getTypeNode()); } /** @@ -427,7 +430,7 @@ public function it_doesnt_fail_on_array_nullable_parameter_with_not_null_default $classNode = $mirror->reflect(new \ReflectionClass('Fixtures\Prophecy\NullableArrayParameter'), array()); $method = $classNode->getMethod('iHaveNullableArrayParameterWithNotNullDefaultValue'); $arguments = $method->getArguments(); - $this->assertEquals(new ArgumentTypeNode('array', 'null'), $arguments[0]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode(new NamedTypeNode('array', true, true)), $arguments[0]->getTypeNode()); } /** @@ -440,7 +443,7 @@ public function it_doesnt_fail_to_typehint_nonexistent_RQCN() $classNode = $mirror->reflect(new \ReflectionClass('Fixtures\Prophecy\OptionalDepsClass'), array()); $method = $classNode->getMethod('iHaveAnEvenStrangerTypeHintedArg'); $arguments = $method->getArguments(); - $this->assertEquals(new ArgumentTypeNode('\I\Simply\Am\Not'), $arguments[0]->getTypeNode()); + $this->assertEquals(new ArgumentTypeNode(new NamedTypeNode('\I\Simply\Am\Not')), $arguments[0]->getTypeNode()); } /** @@ -533,7 +536,10 @@ public function it_can_double_a_class_with_union_return_types() $classNode = (new ClassMirror())->reflect(new \ReflectionClass('Fixtures\Prophecy\UnionReturnTypes'), []); $methodNode = $classNode->getMethods()['doSomething']; - $this->assertSame(['\stdClass', 'bool'], $methodNode->getReturnTypeNode()->getTypes()); + $stdClass = new NamedTypeNode('\stdClass', false, false); + $bool = new NamedTypeNode('bool', false, true); + $union = new UnionTypeNode(false, $stdClass, $bool); + $this->assertEquals($union, $methodNode->getReturnTypeNode()->getType()); } /** @test */ @@ -546,7 +552,11 @@ public function it_can_double_a_class_with_union_argument_types() $classNode = (new ClassMirror())->reflect(new \ReflectionClass('Fixtures\Prophecy\UnionArgumentTypes'), []); $methodNode = $classNode->getMethods()['doSomething']; - $this->assertEquals(new ArgumentTypeNode('\\stdClass', 'bool'), $methodNode->getArguments()[0]->getTypeNode()); + $stdClass = new NamedTypeNode('\stdClass', false, false); + $bool = new NamedTypeNode('bool', false, true); + $union = new UnionTypeNode(false, $stdClass, $bool); + + $this->assertEquals(new ArgumentTypeNode($union), $methodNode->getArguments()[0]->getTypeNode()); } /** @test */ @@ -559,8 +569,8 @@ public function it_can_double_a_class_with_mixed_types() $classNode = (new ClassMirror())->reflect(new \ReflectionClass('Fixtures\Prophecy\MixedTypes'), []); $methodNode = $classNode->getMethods()['doSomething']; - $this->assertEquals(new ArgumentTypeNode('mixed'), $methodNode->getArguments()[0]->getTypeNode()); - $this->assertEquals(new ReturnTypeNode('mixed'), $methodNode->getReturnTypeNode()); + $this->assertEquals(new ArgumentTypeNode(new NamedTypeNode('mixed', true, true)), $methodNode->getArguments()[0]->getTypeNode()); + $this->assertEquals(new ReturnTypeNode(new NamedTypeNode('mixed', true, true)), $methodNode->getReturnTypeNode()); } /** @test */ @@ -569,7 +579,7 @@ public function it_can_double_inherited_self_return_type() $classNode = (new ClassMirror())->reflect(new \ReflectionClass('Fixtures\Prophecy\ClassExtendAbstractWithMethodWithReturnType'), []); $methodNode = $classNode->getMethods()['returnSelf']; - $this->assertEquals(new ReturnTypeNode('\Fixtures\Prophecy\AbstractBaseClassWithMethodWithReturnType'), $methodNode->getReturnTypeNode()); + $this->assertEquals(new ReturnTypeNode(new NamedTypeNode('\Fixtures\Prophecy\AbstractBaseClassWithMethodWithReturnType')), $methodNode->getReturnTypeNode()); } /** @@ -600,7 +610,7 @@ public function it_can_double_never_return_type() $classNode = (new ClassMirror())->reflect(new \ReflectionClass('Fixtures\Prophecy\NeverType'), []); $methodNode = $classNode->getMethods()['doSomething']; - $this->assertEquals(new ReturnTypeNode('never'), $methodNode->getReturnTypeNode()); + $this->assertEquals(new ReturnTypeNode(new NamedTypeNode('never', false, true)), $methodNode->getReturnTypeNode()); } @@ -612,7 +622,7 @@ public function it_can_double_void_return_type() $classNode = (new ClassMirror())->reflect(new \ReflectionClass('Fixtures\Prophecy\VoidReturnType'), []); $methodNode = $classNode->getMethods()['doSomething']; - $this->assertEquals(new ReturnTypeNode('void'), $methodNode->getReturnTypeNode()); + $this->assertEquals(new ReturnTypeNode(new NamedTypeNode('void', false, true)), $methodNode->getReturnTypeNode()); } /** @@ -632,15 +642,20 @@ public function it_can_not_double_an_enum() /** * @test */ - public function it_can_not_double_intersection_return_types() + public function it_can_double_intersection_return_types() { if (PHP_VERSION_ID < 80100) { $this->markTestSkipped('Intersection types are not supported in this PHP version'); } - $this->expectException(ClassMirrorException::class); - $classNode = (new ClassMirror())->reflect(new \ReflectionClass('Fixtures\Prophecy\IntersectionReturnType'), []); + $methodNode = $classNode->getMethods()['doSomething']; + + $bar = new NamedTypeNode('\Fixtures\Prophecy\Bar'); + $baz = new NamedTypeNode('\Fixtures\Prophecy\Baz'); + $intersection = new IntersectionTypeNode(false, $bar, $baz); + + $this->assertEquals(new ReturnTypeNode($intersection), $methodNode->getReturnTypeNode()); } /** @@ -652,8 +667,13 @@ public function it_can_not_double_intersection_argument_types() $this->markTestSkipped('Intersection types are not supported in this PHP version'); } - $this->expectException(ClassMirrorException::class); - $classNode = (new ClassMirror())->reflect(new \ReflectionClass('Fixtures\Prophecy\IntersectionArgumentType'), []); + $methodNode = $classNode->getMethods()['doSomething']; + + $bar = new NamedTypeNode('\Fixtures\Prophecy\Bar'); + $baz = new NamedTypeNode('\Fixtures\Prophecy\Baz'); + $intersection = new IntersectionTypeNode(false, $bar, $baz); + + $this->assertEquals(new ArgumentTypeNode($intersection), $methodNode->getArguments()[0]->getTypeNode()); } }