Skip to content

Commit 4457dba

Browse files
authored
Add support for stubs to declare intersection type class properties (#8751)
1 parent 77bd39a commit 4457dba

File tree

6 files changed

+96
-13
lines changed

6 files changed

+96
-13
lines changed

Zend/tests/type_declarations/typed_properties_095.phpt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Typed properties in internal classes
33
--EXTENSIONS--
44
zend_test
5+
spl
56
--FILE--
67
<?php
78

@@ -70,6 +71,8 @@ object(_ZendTestClass)#1 (3) {
7071
}
7172
["classUnionProp"]=>
7273
NULL
74+
["classIntersectionProp"]=>
75+
uninitialized(Traversable&Countable)
7376
["readonlyProp"]=>
7477
uninitialized(int)
7578
}
@@ -84,6 +87,8 @@ object(Test)#4 (3) {
8487
}
8588
["classUnionProp"]=>
8689
NULL
90+
["classIntersectionProp"]=>
91+
uninitialized(Traversable&Countable)
8792
["readonlyProp"]=>
8893
uninitialized(int)
8994
}

Zend/zend_types.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ typedef struct {
280280
#define ZEND_TYPE_INIT_UNION(ptr, extra_flags) \
281281
{ (void *) (ptr), (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_UNION_BIT) | (extra_flags) }
282282

283+
#define ZEND_TYPE_INIT_INTERSECTION(ptr, extra_flags) \
284+
{ (void *) (ptr), (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_INTERSECTION_BIT) | (extra_flags) }
285+
283286
#define ZEND_TYPE_INIT_CLASS(class_name, allow_null, extra_flags) \
284287
ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags)
285288

build/gen_stub.php

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -548,23 +548,26 @@ public function equals(SimpleType $other): bool {
548548
class Type {
549549
/** @var SimpleType[] */
550550
public $types;
551+
/** @var bool */
552+
public $isIntersection = false;
551553

552554
public static function fromNode(Node $node): Type {
553-
if ($node instanceof Node\UnionType) {
555+
if ($node instanceof Node\UnionType || $node instanceof Node\IntersectionType) {
554556
$nestedTypeObjects = array_map(['Type', 'fromNode'], $node->types);
555557
$types = [];
556558
foreach ($nestedTypeObjects as $typeObject) {
557559
array_push($types, ...$typeObject->types);
558560
}
559-
return new Type($types);
561+
return new Type($types, ($node instanceof Node\IntersectionType));
560562
}
561563

562564
if ($node instanceof Node\NullableType) {
563565
return new Type(
564566
[
565567
...Type::fromNode($node->type)->types,
566568
SimpleType::null(),
567-
]
569+
],
570+
false
568571
);
569572
}
570573

@@ -573,18 +576,20 @@ public static function fromNode(Node $node): Type {
573576
[
574577
SimpleType::fromString("Traversable"),
575578
ArrayType::createGenericArray(),
576-
]
579+
],
580+
false
577581
);
578582
}
579583

580-
return new Type([SimpleType::fromNode($node)]);
584+
return new Type([SimpleType::fromNode($node)], false);
581585
}
582586

583587
public static function fromString(string $typeString): self {
584588
$typeString .= "|";
585589
$simpleTypes = [];
586590
$simpleTypeOffset = 0;
587591
$inArray = false;
592+
$isIntersection = false;
588593

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

607-
if ($char === "|") {
612+
if ($char === "|" || $char === "&") {
613+
$isIntersection = ($char === "&");
608614
$simpleTypeName = trim(substr($typeString, $simpleTypeOffset, $i - $simpleTypeOffset));
609615

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

616-
return new Type($simpleTypes);
622+
return new Type($simpleTypes, $isIntersection);
617623
}
618624

619625
/**
620626
* @param SimpleType[] $types
621627
*/
622-
private function __construct(array $types) {
628+
private function __construct(array $types, bool $isIntersection) {
623629
$this->types = $types;
630+
$this->isIntersection = $isIntersection;
624631
}
625632

626633
public function isScalar(): bool {
@@ -650,7 +657,8 @@ public function getWithoutNull(): Type {
650657
function(SimpleType $type) {
651658
return !$type->isNull();
652659
}
653-
)
660+
),
661+
false
654662
);
655663
}
656664

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

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

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

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

717727
foreach ($this->types as $type) {
718728
$unionTypeElement = $doc->createElement('type', $type->name);
@@ -755,7 +765,8 @@ public function __toString() {
755765
return 'mixed';
756766
}
757767

758-
return implode('|', array_map(
768+
$char = $this->isIntersection ? '&' : '|';
769+
return implode($char, array_map(
759770
function ($type) { return $type->name; },
760771
$this->types)
761772
);
@@ -2237,7 +2248,11 @@ public function getDeclaration(iterable $allConstInfos): string {
22372248

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

2240-
$code .= "\tzend_type property_{$propertyName}_type = ZEND_TYPE_INIT_UNION(property_{$propertyName}_type_list, $typeMaskCode);\n";
2251+
if ($this->type->isIntersection) {
2252+
$code .= "\tzend_type property_{$propertyName}_type = ZEND_TYPE_INIT_INTERSECTION(property_{$propertyName}_type_list, $typeMaskCode);\n";
2253+
} else {
2254+
$code .= "\tzend_type property_{$propertyName}_type = ZEND_TYPE_INIT_UNION(property_{$propertyName}_type_list, $typeMaskCode);\n";
2255+
}
22412256
$typeCode = "property_{$propertyName}_type";
22422257
} else {
22432258
$escapedClassName = $arginfoType->classTypes[0]->toEscapedName();

ext/zend_test/test.stub.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class _ZendTestClass implements _ZendTestInterface {
2121
public int $intProp = 123;
2222
public ?stdClass $classProp = null;
2323
public stdClass|Iterator|null $classUnionProp = null;
24+
public Traversable&Countable $classIntersectionProp;
2425
public readonly int $readonlyProp;
2526

2627
public static function is_object(): int {}

ext/zend_test/test_arginfo.h

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
Test that internal classes can register intersection types
3+
--EXTENSIONS--
4+
zend_test
5+
spl
6+
--FILE--
7+
<?php
8+
9+
class C implements Countable {
10+
public function count(): int {
11+
return 1;
12+
}
13+
}
14+
15+
class I extends EmptyIterator implements Countable {
16+
public function count(): int {
17+
return 1;
18+
}
19+
}
20+
21+
$o = new _ZendTestClass();
22+
23+
try {
24+
var_dump($o->classIntersectionProp);
25+
} catch (Error $e) {
26+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
27+
}
28+
try {
29+
$o->classIntersectionProp = new EmptyIterator();
30+
} catch (TypeError $e) {
31+
echo $e->getMessage(), PHP_EOL;
32+
}
33+
try {
34+
$o->classIntersectionProp = new C();
35+
} catch (TypeError $e) {
36+
echo $e->getMessage(), PHP_EOL;
37+
}
38+
$o->classIntersectionProp = new I();
39+
40+
?>
41+
==DONE==
42+
--EXPECT--
43+
Error: Typed property _ZendTestClass::$classIntersectionProp must not be accessed before initialization
44+
Cannot assign EmptyIterator to property _ZendTestClass::$classIntersectionProp of type Traversable&Countable
45+
Cannot assign C to property _ZendTestClass::$classIntersectionProp of type Traversable&Countable
46+
==DONE==

0 commit comments

Comments
 (0)