Skip to content

Intersection types #637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
use Prophecy\Doubler\Generator\Node\ReturnTypeNode;
use Prophecy\Doubler\Generator\Node\Type\IntersectionType;
use Prophecy\Doubler\Generator\Node\Type\SimpleType;
use Prophecy\Doubler\Generator\Node\Type\UnionType;

class ClassCodeGeneratorSpec extends ObjectBehavior
{
Expand Down Expand Up @@ -115,10 +118,10 @@ class CustomClass extends \RuntimeException implements \Prophecy\Doubler\Generat
public $name;
private $email;

public static function getName(array $fullname, \ReflectionClass $class, object $instance): ?string {
public static function getName(array $fullname, \ReflectionClass $class, object $instance): string|null {
return $this->name;
}
protected function getEmail(?string $default = '[email protected]') {
protected function getEmail(string|null $default = '[email protected]') {
return $this->email;
}
public function &getRefValue( $refValue): string {
Expand Down Expand Up @@ -271,7 +274,7 @@ function it_overrides_properly_methods_with_args_passed_by_reference(
namespace {
class CustomClass extends \RuntimeException implements \Prophecy\Doubler\Generator\MirroredInterface {

public function getName(?array &$fullname = NULL) {
public function getName(array|null &$fullname = NULL) {
return $this->name;
}

Expand Down Expand Up @@ -310,6 +313,47 @@ public function foo(): int|string|null {

}

}
}
PHP;
$expected = strtr($expected, array("\r\n" => "\n", "\r" => "\n"));

$code->shouldBe($expected);
}

function it_generates_proper_code_for_intersection_return_types(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest having separate tests for an intersection return type (what the current name says) and for a DNF type (what it does)

ClassNode $class,
MethodNode $method
) {
$class->getParentClass()->willReturn('stdClass');
$class->getInterfaces()->willReturn([]);
$class->getProperties()->willReturn([]);
$class->getMethods()->willReturn(array($method));
$class->isReadOnly()->willReturn(false);

$method->getName()->willReturn('foo');
$method->getVisibility()->willReturn('public');
$method->isStatic()->willReturn(false);
$method->getArguments()->willReturn([]);
$method->getReturnTypeNode()->willReturn(new ReturnTypeNode(
new UnionType([
new IntersectionType([new SimpleType('Foo'), new SimpleType('Bar')]),
new SimpleType('string'),
])
));
$method->returnsReference()->willReturn(false);
$method->getCode()->willReturn('');

$code = $this->generate('CustomClass', $class);

$expected = <<<'PHP'
namespace {
class CustomClass extends \stdClass implements {

public function foo(): \Foo&\Bar|string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a valid DNF type in the code: https://3v4l.org/aivA5 (I just removed the implements that is invalid code, as our ClassCodeGenerator apparently does not support the case of an empty list of interface, which is not an issue for Prophecy itself as we always add the ProphecySubjectInterface)


}

}
}
PHP;
Expand Down
4 changes: 2 additions & 2 deletions spec/Prophecy/Doubler/Generator/Node/ClassNodeSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ function it_can_has_methods(MethodNode $method1, MethodNode $method2)
$this->addMethod($method1);
$this->addMethod($method2);

$this->getMethods()->shouldReturn(array(
$this->getMethods()->shouldReturn([
'__construct' => $method1,
'getName' => $method2,
));
]);
}

function its_hasMethod_returns_true_if_method_exists(MethodNode $method)
Expand Down
51 changes: 51 additions & 0 deletions spec/Prophecy/Doubler/Generator/Node/Type/IntersectionTypeSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace spec\Prophecy\Doubler\Generator\Node\Type;

use PhpSpec\ObjectBehavior;
use Prophecy\Doubler\Generator\Node\Type\SimpleType;
use Prophecy\Doubler\Generator\Node\Type\TypeInterface;
use Prophecy\Doubler\Generator\Node\Type\UnionType;
use Prophecy\Exception\Doubler\DoubleException;

class IntersectionTypeSpec extends ObjectBehavior
{
function let(): void
{
$this->beConstructedWith([
new SimpleType('Foo'),
new SimpleType('Bar'),
]);
}

function it_should_implement_type_union(): void
{
$this->shouldImplement(TypeInterface::class);
}

function it_should_throw_double_exception_for_builtin_types()
{
$this->beConstructedWith([
new SimpleType('string'),
new SimpleType('Foo'),
]);
$this->shouldThrow(DoubleException::class)->duringInstantiation();
}

function it_should_throw_double_exception_if_less_than_2_types_provided()
{
$this->beConstructedWith([
new SimpleType('Bar'),
]);
$this->shouldThrow(DoubleException::class)->duringInstantiation();
}

function it_should_throw_double_exception_if_union_type_given(): void
{
$this->beConstructedWith([
new SimpleType('Bar'),
new UnionType([new SimpleType('Foo'), new SimpleType('Baz')]),
]);
$this->shouldThrow(DoubleException::class)->duringInstantiation();
}
}
40 changes: 40 additions & 0 deletions spec/Prophecy/Doubler/Generator/Node/Type/SimpleTypeSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace spec\Prophecy\Doubler\Generator\Node\Type;

use PhpSpec\ObjectBehavior;
use Prophecy\Doubler\Generator\Node\Type\TypeInterface;

class SimpleTypeSpec extends ObjectBehavior
{
function let(): void
{
$this->beConstructedWith('string');
}

function it_implements_type_interface(): void
{
$this->shouldImplement(TypeInterface::class);
}

function it_is_stringable(): void
{
$this->beConstructedWith('int');
$this->getType()->shouldReturn('int');
$this->__toString()->shouldReturn('int');
}

function it_prefix_namespace_with_antislash(): void
{
$this->beConstructedWith('Prophecy\\Doubler\\Generator\\Node\\Type\\SimpleType');
$this->getType()->shouldReturn('\\Prophecy\\Doubler\\Generator\\Node\\Type\\SimpleType');
$this->isBuiltin()->shouldReturn(false);
}

function it_resolves_builtin_aliases(): void
{
$this->beConstructedWith('double');
$this->getType()->shouldReturn('float');
$this->isBuiltin()->shouldReturn(true);
}
}
82 changes: 82 additions & 0 deletions spec/Prophecy/Doubler/Generator/Node/Type/UnionTypeSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace spec\Prophecy\Doubler\Generator\Node\Type;

use PhpSpec\ObjectBehavior;
use Prophecy\Doubler\Generator\Node\Type\IntersectionType;
use Prophecy\Doubler\Generator\Node\Type\SimpleType;
use Prophecy\Doubler\Generator\Node\Type\TypeInterface;
use Prophecy\Doubler\Generator\Node\Type\UnionType;
use Prophecy\Exception\Doubler\DoubleException;

class UnionTypeSpec extends ObjectBehavior
{
function let(): void
{
$this->beConstructedWith([
new SimpleType('int'),
new SimpleType('string'),
]);
}
function it_implements_type_interface(): void
{
$this->shouldImplement(TypeInterface::class);
}

function it_throws_double_exception_when_union_type_given(): void
{
$this->beConstructedWith([
new UnionType([new SimpleType('int'), new SimpleType('string')]),
new SimpleType('bool'),
]);
$this->shouldThrow(DoubleException::class)->duringInstantiation();
}

function it_throws_double_exception_when_types_duplicated(): void
{
$this->beConstructedWith([new SimpleType('string'), new SimpleType('string')]);
$this->shouldThrow(DoubleException::class)->duringInstantiation();
}

function it_throws_double_exception_when_union_with_void(): void
{
$this->beConstructedWith([new SimpleType('void'), new SimpleType('string')]);
$this->shouldThrow(DoubleException::class)->duringInstantiation();
}

function it_throws_double_exception_when_union_with_never(): void
{
$this->beConstructedWith([new SimpleType('never'), new SimpleType('string')]);
$this->shouldThrow(DoubleException::class)->duringInstantiation();
}

function it_throws_double_exception_when_union_with_mixed(): void
{
$this->beConstructedWith([new SimpleType('mixed'), new SimpleType('string')]);
$this->shouldThrow(DoubleException::class)->duringInstantiation();
}

function it_throws_double_exception_when_union_with_only_one_type(): void
{
$this->beConstructedWith([new SimpleType('string')]);
$this->shouldThrow(DoubleException::class)->duringInstantiation();
}

function it_return_array_of_its_types(): void
{
$this->getTypes()->shouldBeLike([
new SimpleType('int'),
new SimpleType('string'),
]);
}

function it_should_accept_simple_type_and_intersection()
{
$type1 = new SimpleType('string');
$type2 = new IntersectionType([new SimpleType('A'), new SimpleType('B')]);
$this->beConstructedWith([$type1, $type2]);

$this->has($type1)->shouldBe(true);
$this->has($type2)->shouldBe(true);
}
}
37 changes: 31 additions & 6 deletions src/Prophecy/Doubler/Generator/ClassCodeGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
namespace Prophecy\Doubler\Generator;

use Prophecy\Doubler\Generator\Node\ReturnTypeNode;
use Prophecy\Doubler\Generator\Node\Type\IntersectionType;
use Prophecy\Doubler\Generator\Node\Type\SimpleType;
use Prophecy\Doubler\Generator\Node\Type\TypeInterface;
use Prophecy\Doubler\Generator\Node\Type\UnionType;
use Prophecy\Doubler\Generator\Node\TypeNodeAbstract;
use Prophecy\Exception\Doubler\ClassCreatorException;

/**
* Class code creator.
Expand Down Expand Up @@ -78,16 +83,36 @@

private function generateTypes(TypeNodeAbstract $typeNode): string
{
if (!$typeNode->getTypes()) {
if ($typeNode->getType() === null) {
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]);
} else {
return join('|', $typeNode->getTypes());
$generatedType = $this->generateSubType($typeNode->getType());

return $generatedType;
}

private function generateSubType(TypeInterface $type): string
{
if ($type instanceof SimpleType) {
return $type->getType();
}

if ($type instanceof UnionType) {
return join('|', array_map(
fn(TypeInterface $type) => $this->generateSubType($type),
$type->getTypes()
));
}

if ($type instanceof IntersectionType) {
return join('&', array_map(
fn(SimpleType $type) => $type->getType(),
$type->getTypes()
));
}

throw new ClassCreatorException(sprintf('Type "%s" is not supported.', get_class($type)));

Check failure on line 115 in src/Prophecy/Doubler/Generator/ClassCodeGenerator.php

View workflow job for this annotation

GitHub Actions / Static analysis

Class Prophecy\Exception\Doubler\ClassCreatorException constructor invoked with 1 parameter, 2 required.
}

/**
Expand Down
Loading