-
Notifications
You must be signed in to change notification settings - Fork 242
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
base: master
Are you sure you want to change the base?
Intersection types #637
Conversation
$node->setReturnTypeNode(new ReturnTypeNode(...$returnTypes)); | ||
// Tentative return types also need reflection | ||
$returnReflectionType = $method->getTentativeReturnType(); | ||
\assert($returnReflectionType !== null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think those asserts are useless (the $returnReflectionType
variable will still be nullable anyway as there is no else
branch assigning it)
{ | ||
if ($type instanceof ReflectionIntersectionType) { | ||
foreach ($type->getTypes() as $innerReflectionType) { | ||
$innerTypes[] = new SimpleType($innerReflectionType->getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't this missing the resolution of the type name ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe this should use createTypeFromReflection
for the inner type as well.
return $this->types !== ['void' => 'void'] | ||
&& $this->types !== ['never' => 'never']; | ||
if ($this->type === null) { | ||
return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be true
/** | ||
* @param string|TypeInterface ...$types | ||
*/ | ||
public function __construct(string|TypeInterface ...$types) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't the new API allows null
as well ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep the new API will be null|string|TypeInterface $type = null
, you're absolutely right
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then this needs to be updated to support passing null
} | ||
|
||
public function canUseNullShorthand(): bool | ||
{ | ||
return isset($this->types['null']) && count($this->types) === 2; | ||
if ($this->type instanceof UnionType) { | ||
return $this->type->has(new SimpleType('null')) && count($this->type->getTypes()) === 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the union contains an intersection type as its other type ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not see any problem here but code generation needs an update for sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah true. We can use the null shorthand on an intersection. I forgot that.
throw new DoubleException('Type cannot be nullable true'); | ||
} | ||
} | ||
|
||
if (\PHP_VERSION_ID >= 80000 && isset($this->types['mixed']) && count($this->types) !== 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why removing this ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question is more like why is this method guardIsValidType
still here. I will remove it on cleanup. The UnionType checks this mixed-thing already.
return $this->prefixWithNsSeparator($type); | ||
} | ||
} | ||
|
||
/** | ||
* @todo: put this in SimpleType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cannot really go in SimpleType, as some of those are about the full type, not each simple type used in a union
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess each type will have its own exceptions, this still needs to be done.
return $this->prefixWithNsSeparator($type); | ||
} | ||
} | ||
|
||
/** | ||
* @todo: put this in SimpleType | ||
* @return void | ||
*/ | ||
protected function guardIsValidType() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Be careful. ReturnTypeNode extends this class to override this method, but you still use $this->types
in it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After cleanup this method should not even exist anymore here.
|
||
namespace Prophecy\Doubler\Generator\Node\Type; | ||
|
||
class SimpleType implements TypeInterface, \Stringable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think TypeInterface should actually extend Stringable to force all types to be castable to string (and documenting that we expect their string representation to be usable in a namespaced context)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had this idea at the begining but I ended up with this because I don't want to make code generation in tostring methods. Actually I think I will remove stringable even from here.
return 'int'; | ||
|
||
// built in types | ||
case 'self': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self
(and parent
) should be resolved to the actual class name before creating the type from reflection, to avoid issues (Prophecy will use those types to generate methods in a child class, where those keywords would have a different meaning, which would break in parameter types)
5250a7c
to
ebb7746
Compare
Thank you for your review @stof . And good news! I fixed the bugs in my previous implementation. (some you noticed in your review!) Keeping backward compatibility has been a real pain! But it WORKS. (test suite green 100%) It's currently a fully working PR for intersection type.... But only for return types! Some work needs to be done for cleaning... But it also needs php to be bumped to 8.1+ (which would probably be better in another PR), and a lot of cleaning. But for now, since it's a lot of work and personal investment, I'd like you to tell me if the implementation is ok before working on it again. |
e0672c9
to
facea20
Compare
|
||
namespace Prophecy\Doubler\Generator\Node\Type; | ||
|
||
class SimpleType implements TypeInterface, \Stringable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To simplify code generation, I think we should have 2 separate Type classes for that:
- BuiltinType to represent built-in types
- ObjectType to represent object-based types, taking a class name as argument (and prefixing it with a
\
when rendering it to be compatible with namespaced context, but not when returning it in agetClass
method so that we can compare that getter toFoo::class
if needed, unlike the currentgetType
method)
Both classes can of course implement a SimpleType interface containing a method returning the code representation of the type. But then, I think it could make sense to have that method returning such code representation be part of TypeInterface and let UnionType and IntersectionType implement it (basically, using the logic you have in the ClassCodeGenerator for now)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having an ObjectType and BuiltinType looks over-engineered to me (a lot of code for basically the same thing). It also involve a factory in the middle making the distinction between the 2 cases (in classmirror).
About the problem you mention, it would be possible to normalize the type only in the toString method, isn't it?
I understand that having the 2 types makes sense. I actually like pretty much the idea, I just worry of the "why" and "isn't it just more code for more code".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ClassMirror could create the right type based on ReflectionNamedType::isBuiltIn()
, which would also mean we automatically get support for new builtin types for newer PHP versions instead of having to hardcode which strings are a built-in type that need to produce a different code representation (classes need to be prefixed with \
to be used in a namespaced context while built-in type must not be prefixed).
use Prophecy\Exception\Doubler\DoubleException; | ||
|
||
abstract class TypeNodeAbstract | ||
{ | ||
/** @var array<string, string> */ | ||
protected $types = []; | ||
protected TypeInterface|null $type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest making the new property private (if child classes need to read it, there is a public getter they can use)
/** | ||
* @param string|TypeInterface ...$types | ||
*/ | ||
public function __construct(string|TypeInterface ...$types) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then this needs to be updated to support passing null
} | ||
|
||
public function canUseNullShorthand(): bool | ||
{ | ||
return isset($this->types['null']) && count($this->types) === 2; | ||
if ($this->type instanceof UnionType) { | ||
return $this->type->has(new SimpleType('null')) && count($this->type->getTypes()) === 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah true. We can use the null shorthand on an intersection. I forgot that.
|
||
if ($type instanceof UnionType) { | ||
return join('|', array_map( | ||
fn (TypeInterface $type) => $this->generateSubType($type), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this logic does not support DNF types. Parenthesis are required around the intersection types used in a union type.
$this->builtin = true; | ||
switch ($type) { | ||
// type aliases | ||
case 'double': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
double
is not a builtin alias in PHP. It is treated as a class name (and triggers a warning). Same for the others: https://3v4l.org/T9iRf
I think the new API should not have those special cases (the BC layer in TypeNodeAbstract should handle it instead, and triggering an additional deprecation saying that BuiltinType
in the new API will not support such aliases and they should use the proper value instead)
namespace { | ||
class CustomClass extends \stdClass implements { | ||
|
||
public function foo(): \Foo&\Bar|string { |
There was a problem hiding this comment.
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)
$code->shouldBe($expected); | ||
} | ||
|
||
function it_generates_proper_code_for_intersection_return_types( |
There was a problem hiding this comment.
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)
Note about the design: I created a new sub-api for type management because the current one is not compatible with intersections but needs to exist to keep backward compatibility.
What still needs to be done:
Please have a look and tell me if this kind of implementation would be ok for you. Thanks!
Stands as replacement for #569 and should fix #535 and #558