From 467ebda9c6de9031c99af67ffca18c713f453633 Mon Sep 17 00:00:00 2001 From: George Peter Banyard Date: Sat, 11 Jun 2022 17:12:36 +0100 Subject: [PATCH 1/3] Add support for stubs to declare intersection type class properties --- Zend/zend_types.h | 3 ++ build/gen_stub.php | 16 ++++++- ext/zend_test/test.stub.php | 1 + ext/zend_test/test_arginfo.h | 15 +++++- ...zend_internal_class_prop_intersection.phpt | 46 +++++++++++++++++++ 5 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 ext/zend_test/tests/zend_internal_class_prop_intersection.phpt diff --git a/Zend/zend_types.h b/Zend/zend_types.h index 908a2c769a909..85a768c3da1ef 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -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) diff --git a/build/gen_stub.php b/build/gen_stub.php index cf04aa2c148e1..5cd9da4f5b31e 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -558,6 +558,14 @@ public static function fromNode(Node $node): Type { } return new Type($types); } + if ($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, true); + } if ($node instanceof Node\NullableType) { return new Type( @@ -619,7 +627,7 @@ public static function fromString(string $typeString): self { /** * @param SimpleType[] $types */ - private function __construct(array $types) { + private function __construct(array $types, public readonly bool $isIntersection = false) { $this->types = $types; } @@ -2237,7 +2245,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(); diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index 64b4f65648be7..d6890b52ffa2f 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -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; public readonly int $readonlyProp; public static function is_object(): int {} diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index 82998352d1f57..79c337e31886d 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 7c248caeb6f63cee014ece1b59c481a58d150e72 */ + * Stub hash: 3855d7a038193445e6d881b157ac902deedf3676 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() @@ -352,6 +352,19 @@ static zend_class_entry *register_class__ZendTestClass(zend_class_entry *class_e zend_declare_typed_property(class_entry, property_classUnionProp_name, &property_classUnionProp_default_value, ZEND_ACC_PUBLIC, NULL, property_classUnionProp_type); zend_string_release(property_classUnionProp_name); + zend_string *property_classIntersectionProp_class_Traversable = zend_string_init("Traversable", sizeof("Traversable") - 1, 1); + zend_string *property_classIntersectionProp_class_Countable = zend_string_init("Countable", sizeof("Countable") - 1, 1); + zend_type_list *property_classIntersectionProp_type_list = malloc(ZEND_TYPE_LIST_SIZE(2)); + property_classIntersectionProp_type_list->num_types = 2; + property_classIntersectionProp_type_list->types[0] = (zend_type) ZEND_TYPE_INIT_CLASS(property_classIntersectionProp_class_Traversable, 0, 0); + property_classIntersectionProp_type_list->types[1] = (zend_type) ZEND_TYPE_INIT_CLASS(property_classIntersectionProp_class_Countable, 0, 0); + zend_type property_classIntersectionProp_type = ZEND_TYPE_INIT_INTERSECTION(property_classIntersectionProp_type_list, 0); + zval property_classIntersectionProp_default_value; + ZVAL_UNDEF(&property_classIntersectionProp_default_value); + zend_string *property_classIntersectionProp_name = zend_string_init("classIntersectionProp", sizeof("classIntersectionProp") - 1, 1); + zend_declare_typed_property(class_entry, property_classIntersectionProp_name, &property_classIntersectionProp_default_value, ZEND_ACC_PUBLIC, NULL, property_classIntersectionProp_type); + zend_string_release(property_classIntersectionProp_name); + zval property_readonlyProp_default_value; ZVAL_UNDEF(&property_readonlyProp_default_value); zend_string *property_readonlyProp_name = zend_string_init("readonlyProp", sizeof("readonlyProp") - 1, 1); diff --git a/ext/zend_test/tests/zend_internal_class_prop_intersection.phpt b/ext/zend_test/tests/zend_internal_class_prop_intersection.phpt new file mode 100644 index 0000000000000..dd9fe93a130eb --- /dev/null +++ b/ext/zend_test/tests/zend_internal_class_prop_intersection.phpt @@ -0,0 +1,46 @@ +--TEST-- +Test that internal classes can register intersection types +--EXTENSIONS-- +zend_test +spl +--FILE-- +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== From 8f14d0f866a385514f29813a6398c31a9b570873 Mon Sep 17 00:00:00 2001 From: George Peter Banyard Date: Sat, 11 Jun 2022 17:37:31 +0100 Subject: [PATCH 2/3] Adjust test expectation --- Zend/tests/type_declarations/typed_properties_095.phpt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Zend/tests/type_declarations/typed_properties_095.phpt b/Zend/tests/type_declarations/typed_properties_095.phpt index a03bcf9555276..5caf862e72da7 100644 --- a/Zend/tests/type_declarations/typed_properties_095.phpt +++ b/Zend/tests/type_declarations/typed_properties_095.phpt @@ -2,6 +2,7 @@ Typed properties in internal classes --EXTENSIONS-- zend_test +spl --FILE-- NULL + ["classIntersectionProp"]=> + uninitialized(Traversable&Countable) ["readonlyProp"]=> uninitialized(int) } @@ -84,6 +87,8 @@ object(Test)#4 (3) { } ["classUnionProp"]=> NULL + ["classIntersectionProp"]=> + uninitialized(Traversable&Countable) ["readonlyProp"]=> uninitialized(int) } From 9aaa860e574466a002811ba11add956a70996d47 Mon Sep 17 00:00:00 2001 From: George Peter Banyard Date: Sat, 18 Jun 2022 13:17:24 +0100 Subject: [PATCH 3/3] Address reviews --- build/gen_stub.php | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/build/gen_stub.php b/build/gen_stub.php index 5cd9da4f5b31e..9103ba42191d7 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -548,23 +548,17 @@ 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); - } - if ($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, true); + return new Type($types, ($node instanceof Node\IntersectionType)); } if ($node instanceof Node\NullableType) { @@ -572,7 +566,8 @@ public static function fromNode(Node $node): Type { [ ...Type::fromNode($node->type)->types, SimpleType::null(), - ] + ], + false ); } @@ -581,11 +576,12 @@ 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 { @@ -593,6 +589,7 @@ public static function fromString(string $typeString): self { $simpleTypes = []; $simpleTypeOffset = 0; $inArray = false; + $isIntersection = false; $typeStringLength = strlen($typeString); for ($i = 0; $i < $typeStringLength; $i++) { @@ -612,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); @@ -621,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, public readonly bool $isIntersection = false) { + private function __construct(array $types, bool $isIntersection) { $this->types = $types; + $this->isIntersection = $isIntersection; } public function isScalar(): bool { @@ -658,7 +657,8 @@ public function getWithoutNull(): Type { function(SimpleType $type) { return !$type->isNull(); } - ) + ), + false ); } @@ -691,6 +691,7 @@ public function toOptimizerTypeMask(): string { $optimizerTypes = []; foreach ($this->types as $type) { + // TODO Support for toOptimizerMask for intersection $optimizerTypes[] = $type->toOptimizerTypeMask(); } @@ -719,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); @@ -763,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) );