Skip to content

Add support for stubs to declare intersection type class properties #8751

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

Merged
merged 3 commits into from
Jul 22, 2022
Merged
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
5 changes: 5 additions & 0 deletions Zend/tests/type_declarations/typed_properties_095.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Typed properties in internal classes
--EXTENSIONS--
zend_test
spl
--FILE--
<?php

Expand Down Expand Up @@ -70,6 +71,8 @@ object(_ZendTestClass)#1 (3) {
}
["classUnionProp"]=>
NULL
["classIntersectionProp"]=>
uninitialized(Traversable&Countable)
["readonlyProp"]=>
uninitialized(int)
}
Expand All @@ -84,6 +87,8 @@ object(Test)#4 (3) {
}
["classUnionProp"]=>
NULL
["classIntersectionProp"]=>
uninitialized(Traversable&Countable)
["readonlyProp"]=>
uninitialized(int)
}
Expand Down
3 changes: 3 additions & 0 deletions Zend/zend_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ typedef struct {
#define ZEND_TYPE_INIT_UNION(ptr, extra_flags) \
{ (void *) (ptr), (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_UNION_BIT) | (extra_flags) }

#define ZEND_TYPE_INIT_INTERSECTION(ptr, extra_flags) \
{ (void *) (ptr), (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_INTERSECTION_BIT) | (extra_flags) }

#define ZEND_TYPE_INIT_CLASS(class_name, allow_null, extra_flags) \
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags)

Expand Down
39 changes: 27 additions & 12 deletions build/gen_stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -548,23 +548,26 @@ public function equals(SimpleType $other): bool {
class Type {
/** @var SimpleType[] */
public $types;
/** @var bool */
public $isIntersection = false;

public static function fromNode(Node $node): Type {
if ($node instanceof Node\UnionType) {
if ($node instanceof Node\UnionType || $node instanceof Node\IntersectionType) {
$nestedTypeObjects = array_map(['Type', 'fromNode'], $node->types);
$types = [];
foreach ($nestedTypeObjects as $typeObject) {
array_push($types, ...$typeObject->types);
}
return new Type($types);
return new Type($types, ($node instanceof Node\IntersectionType));
}

if ($node instanceof Node\NullableType) {
return new Type(
[
...Type::fromNode($node->type)->types,
SimpleType::null(),
]
],
false
);
}

Expand All @@ -573,18 +576,20 @@ public static function fromNode(Node $node): Type {
[
SimpleType::fromString("Traversable"),
ArrayType::createGenericArray(),
]
],
false
);
}

return new Type([SimpleType::fromNode($node)]);
return new Type([SimpleType::fromNode($node)], false);
}

public static function fromString(string $typeString): self {
$typeString .= "|";
$simpleTypes = [];
$simpleTypeOffset = 0;
$inArray = false;
$isIntersection = false;

$typeStringLength = strlen($typeString);
for ($i = 0; $i < $typeStringLength; $i++) {
Expand All @@ -604,7 +609,8 @@ public static function fromString(string $typeString): self {
continue;
}

if ($char === "|") {
if ($char === "|" || $char === "&") {
$isIntersection = ($char === "&");
$simpleTypeName = trim(substr($typeString, $simpleTypeOffset, $i - $simpleTypeOffset));

$simpleTypes[] = SimpleType::fromString($simpleTypeName);
Expand All @@ -613,14 +619,15 @@ public static function fromString(string $typeString): self {
}
}

return new Type($simpleTypes);
return new Type($simpleTypes, $isIntersection);
}

/**
* @param SimpleType[] $types
*/
private function __construct(array $types) {
private function __construct(array $types, bool $isIntersection) {
$this->types = $types;
$this->isIntersection = $isIntersection;
}

public function isScalar(): bool {
Expand Down Expand Up @@ -650,7 +657,8 @@ public function getWithoutNull(): Type {
function(SimpleType $type) {
return !$type->isNull();
}
)
),
false
);
}

Expand Down Expand Up @@ -683,6 +691,7 @@ public function toOptimizerTypeMask(): string {
$optimizerTypes = [];

foreach ($this->types as $type) {
// TODO Support for toOptimizerMask for intersection
$optimizerTypes[] = $type->toOptimizerTypeMask();
}

Expand Down Expand Up @@ -711,8 +720,9 @@ public function toOptimizerTypeMaskForArrayValue(): string {

public function getTypeForDoc(DOMDocument $doc): DOMElement {
if (count($this->types) > 1) {
$typeSort = $this->isIntersection ? "intersection" : "union";
$typeElement = $doc->createElement('type');
$typeElement->setAttribute("class", "union");
$typeElement->setAttribute("class", $typeSort);

foreach ($this->types as $type) {
$unionTypeElement = $doc->createElement('type', $type->name);
Expand Down Expand Up @@ -755,7 +765,8 @@ public function __toString() {
return 'mixed';
}

return implode('|', array_map(
$char = $this->isIntersection ? '&' : '|';
return implode($char, array_map(
function ($type) { return $type->name; },
$this->types)
);
Expand Down Expand Up @@ -2237,7 +2248,11 @@ public function getDeclaration(iterable $allConstInfos): string {

$typeMaskCode = $this->type->toArginfoType()->toTypeMask();

$code .= "\tzend_type property_{$propertyName}_type = ZEND_TYPE_INIT_UNION(property_{$propertyName}_type_list, $typeMaskCode);\n";
if ($this->type->isIntersection) {
$code .= "\tzend_type property_{$propertyName}_type = ZEND_TYPE_INIT_INTERSECTION(property_{$propertyName}_type_list, $typeMaskCode);\n";
} else {
$code .= "\tzend_type property_{$propertyName}_type = ZEND_TYPE_INIT_UNION(property_{$propertyName}_type_list, $typeMaskCode);\n";
}
$typeCode = "property_{$propertyName}_type";
} else {
$escapedClassName = $arginfoType->classTypes[0]->toEscapedName();
Expand Down
1 change: 1 addition & 0 deletions ext/zend_test/test.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class _ZendTestClass implements _ZendTestInterface {
public int $intProp = 123;
public ?stdClass $classProp = null;
public stdClass|Iterator|null $classUnionProp = null;
public Traversable&Countable $classIntersectionProp;
Copy link
Member

Choose a reason for hiding this comment

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

Do you think it's worthwhile to also add a function with intersection param and return types?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure how to do the check with ZPP or is it possible to them manually?

Copy link
Member

@kocsismate kocsismate Jul 4, 2022

Choose a reason for hiding this comment

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

Yeah, I think you should rely on manual param parsing for now, but we could add a ZPP when we will have a real use-case for an internal intersection param type.

public readonly int $readonlyProp;

public static function is_object(): int {}
Expand Down
15 changes: 14 additions & 1 deletion ext/zend_test/test_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions ext/zend_test/tests/zend_internal_class_prop_intersection.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
--TEST--
Test that internal classes can register intersection types
--EXTENSIONS--
zend_test
spl
--FILE--
<?php

class C implements Countable {
public function count(): int {
return 1;
}
}

class I extends EmptyIterator implements Countable {
public function count(): int {
return 1;
}
}

$o = new _ZendTestClass();

try {
var_dump($o->classIntersectionProp);
} catch (Error $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
try {
$o->classIntersectionProp = new EmptyIterator();
} catch (TypeError $e) {
echo $e->getMessage(), PHP_EOL;
}
try {
$o->classIntersectionProp = new C();
} catch (TypeError $e) {
echo $e->getMessage(), PHP_EOL;
}
$o->classIntersectionProp = new I();

?>
==DONE==
--EXPECT--
Error: Typed property _ZendTestClass::$classIntersectionProp must not be accessed before initialization
Cannot assign EmptyIterator to property _ZendTestClass::$classIntersectionProp of type Traversable&Countable
Cannot assign C to property _ZendTestClass::$classIntersectionProp of type Traversable&Countable
==DONE==