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 @@ +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 0031fe809..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); @@ -135,6 +139,7 @@ public function returnObject(): object { } PHP; + $expected = strtr($expected, array("\r\n" => "\n", "\r" => "\n")); $code->shouldBe($expected); } @@ -205,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); @@ -262,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' @@ -295,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(''); @@ -337,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 9f9f6bbbe..120e85d5e 100644 --- a/spec/Prophecy/Doubler/Generator/Node/ArgumentTypeNodeSpec.php +++ b/spec/Prophecy/Doubler/Generator/Node/ArgumentTypeNodeSpec.php @@ -4,97 +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']); - } - - function it_can_have_multiple_types() - { - $this->beConstructedWith('int', 'string'); - - $this->getTypes()->shouldReturn(['int', 'string']); + $node = new NamedTypeNode('int', false, true); + $this->beConstructedWith($node); + $this->getType()->shouldReturn($node); } - function it_will_prefix_fcqns() + function it_can_have_multiple_union_types() { - $this->beConstructedWith('Foo'); + $int = new NamedTypeNode('int', false, true); + $string = new NamedTypeNode('string', false, true); + $union = new UnionTypeNode(false, $int, $string); + $this->beConstructedWith($union); - $this->getTypes()->shouldReturn(['\\Foo']); + $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(); @@ -103,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/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..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_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()->shouldReturn(['float', 'bool', 'int']); + $this->getType()->shouldReturn($union); } - function it_will_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()->shouldReturn(['\\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 7573ca50e..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 5d9cd2d20..db4e1a6ee 100644 --- a/src/Prophecy/Doubler/Generator/ClassMirror.php +++ b/src/Prophecy/Doubler/Generator/ClassMirror.php @@ -12,7 +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; @@ -149,12 +155,14 @@ private function reflectMethodToNode(ReflectionMethod $method, Node\ClassNode $c } if ($method->hasReturnType()) { - $returnTypes = $this->getTypeHints($method->getReturnType(), $method->getDeclaringClass(), $method->getReturnType()->allowsNull()); - $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()); - $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)) { @@ -171,9 +179,12 @@ 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->getTypeDeclarations(new ArgumentTypeNameNormalization(), $parameter->getDeclaringClass(), $parameter->getType()); + $typeHints = array_shift($typeHints); - $node->setTypeNode(new ArgumentTypeNode(...$typeHints)); + $node->setTypeNode(new ArgumentTypeNode($typeHints)); + } if ($parameter->isVariadic()) { $node->setAsVariadic(); @@ -213,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 new file mode 100644 index 000000000..cefb49001 --- /dev/null +++ b/src/Prophecy/Doubler/Generator/Node/NameNormalization.php @@ -0,0 +1,10 @@ +getRealType($type); + $normalizedTypes[$type] = $type; + } + + return array_values($normalizedTypes); + } + + public 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..31bcc2212 --- /dev/null +++ b/src/Prophecy/Doubler/Generator/Node/NameNormalization/ReturnTypeNameNormalization.php @@ -0,0 +1,17 @@ +types['void']) && count($this->types) !== 1) { - throw new DoubleException('void cannot be part of a union'); - } - if (isset($this->types['never']) && 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(); @@ -34,12 +32,22 @@ protected function guardIsValidType() */ public function isVoid() { - return $this->types == ['void' => 'void']; + return $this->type instanceof NamedTypeNode + && $this->type->getName() == 'void'; } public function hasReturnStatement(): bool { - return $this->types !== ['void' => 'void'] - && $this->types !== ['never' => '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 97fc54978..4153a5771 100644 --- a/src/Prophecy/Doubler/Generator/Node/TypeNodeAbstract.php +++ b/src/Prophecy/Doubler/Generator/Node/TypeNodeAbstract.php @@ -2,96 +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) { - foreach ($types as $type) { - $type = $this->getRealType($type); - $this->types[$type] = $type; - } - + $this->type = $type; $this->guardIsValidType(); } public function canUseNullShorthand(): bool { - return isset($this->types['null']) && count($this->types) <= 2; - } - - public function getTypes(): array - { - return array_values($this->types); + return $this->type instanceof NamedTypeNode + && $this->type->getName() !== 'mixed' + && $this->type->allowsNull(); } - public function getNonNullTypes(): array + public function getType(): ?Type { - $nonNullTypes = $this->types; - unset($nonNullTypes['null']); - - return array_values($nonNullTypes); - } - - protected function prefixWithNsSeparator(string $type): string - { - return '\\' . ltrim($type, '\\'); - } - - 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); - } + return $this->type; } protected function guardIsValidType() { - if ($this->types == ['null' => '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->types == ['false' => 'false']) { - throw new DoubleException('Type cannot be standalone false'); - } - - if ($this->types == ['false' => 'false', 'null' => 'null']) { - throw new DoubleException('Type cannot be nullable 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 (\PHP_VERSION_ID >= 80000 && isset($this->types['mixed']) && 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 03f5964ec..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('bool', '\\stdClass'), $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,8 +610,19 @@ 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()); + + } + + /** + * @test + */ + 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(new NamedTypeNode('void', false, true)), $methodNode->getReturnTypeNode()); } /** @@ -621,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()); } /** @@ -641,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()); } }