diff --git a/UPGRADING b/UPGRADING index 757e29e359320..512a78f9f7e26 100644 --- a/UPGRADING +++ b/UPGRADING @@ -274,6 +274,13 @@ PHP 8.2 UPGRADE NOTES 13. Other Changes ======================================== +- Core: + . The iterable type is now a built-in compile time alias for array|Traversable. + Error messages relating to iterable will therefore now use array|Traversable. + Type Reflection is preserved for single iterable (and ?iterable) to produce + a ReflectionNamedType with name iterable, however usage of iterable in + union types will be converted to array|Traversable + ======================================== 14. Performance Improvements ======================================== diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c index 95c3516b408a7..c17c73ad0efcd 100644 --- a/Zend/Optimizer/zend_inference.c +++ b/Zend/Optimizer/zend_inference.c @@ -2114,9 +2114,6 @@ static uint32_t zend_convert_type_declaration_mask(uint32_t type_mask) { if (type_mask & MAY_BE_CALLABLE) { result_mask |= MAY_BE_STRING|MAY_BE_OBJECT|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF; } - if (type_mask & MAY_BE_ITERABLE) { - result_mask |= MAY_BE_OBJECT|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF; - } if (type_mask & MAY_BE_STATIC) { result_mask |= MAY_BE_OBJECT; } diff --git a/Zend/tests/return_types/generators001.phpt b/Zend/tests/return_types/generators001.phpt index 64793eaa00ff1..615dabc240bee 100644 --- a/Zend/tests/return_types/generators001.phpt +++ b/Zend/tests/return_types/generators001.phpt @@ -26,6 +26,10 @@ function test6() : object|callable { yield 6; } +function test7() : iterable { + yield 7; +} + var_dump( test1(), test2(), @@ -33,6 +37,7 @@ var_dump( test4(), test5(), test6(), + test7(), ); ?> --EXPECTF-- @@ -48,3 +53,5 @@ object(Generator)#%d (%d) { } object(Generator)#%d (%d) { } +object(Generator)#%d (%d) { +} diff --git a/Zend/tests/type_declarations/dnf_types/dnf_2_intersection.phpt b/Zend/tests/type_declarations/dnf_types/dnf_2_intersection.phpt new file mode 100644 index 0000000000000..6375c60ab71bb --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/dnf_2_intersection.phpt @@ -0,0 +1,63 @@ +--TEST-- +Union of two intersection type +--FILE-- +getMessage(), \PHP_EOL; +} +try { + bar2(); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +?> +--EXPECTF-- +object(A)#%d (0) { +} +object(A)#%d (0) { +} +object(B)#%d (0) { +} +object(B)#%d (0) { +} +bar1(): Return value must be of type (X&Y)|(W&Z), C returned +bar2(): Return value must be of type (W&Z)|(X&Y), C returned diff --git a/Zend/tests/type_declarations/dnf_types/dnf_intersection_and_null.phpt b/Zend/tests/type_declarations/dnf_types/dnf_intersection_and_null.phpt new file mode 100644 index 0000000000000..e9089c130b492 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/dnf_intersection_and_null.phpt @@ -0,0 +1,74 @@ +--TEST-- +Union of null and intersection type +--FILE-- +foo1($a); +$test->foo2($a); +$test->foo1($n); +$test->foo2($n); +$test->prop1 = $a; +$test->prop1 = $n; +$test->prop2 = $a; +$test->prop2 = $n; + +$c = new C(); +try { + $test->foo1($c); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + $test->foo2($c); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + $test->prop1 = $c; +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + $test->prop2 = $c; +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +?> +===DONE=== +--EXPECTF-- +object(A)#2 (0) { +} +object(A)#2 (0) { +} +NULL +NULL +Test::foo1(): Argument #1 ($v) must be of type (X&Y)|null, C given, called in %s on line %d +Test::foo2(): Argument #1 ($v) must be of type (X&Y)|null, C given, called in %s on line %d +Cannot assign C to property Test::$prop1 of type (X&Y)|null +Cannot assign C to property Test::$prop2 of type (X&Y)|null +===DONE=== diff --git a/Zend/tests/type_declarations/dnf_types/dnf_intersection_and_single.phpt b/Zend/tests/type_declarations/dnf_types/dnf_intersection_and_single.phpt new file mode 100644 index 0000000000000..4c9a5f95e30d0 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/dnf_intersection_and_single.phpt @@ -0,0 +1,127 @@ +--TEST-- +Union of a simple and intersection type +--FILE-- +foo1($a); +$test->foo2($a); +$test->foo1($i); +$test->foo2($i); +$test->prop1 = $a; +$test->prop1 = $i; +$test->prop2 = $a; +$test->prop2 = $i; + +$test->bar1($a); +$test->bar2($a); +$test->bar1($b); +$test->bar2($b); +$test->prop3 = $a; +$test->prop4 = $b; +$test->prop3 = $a; +$test->prop4 = $b; + +$c = new C(); +try { + $test->foo1($c); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + $test->foo2($c); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + $test->bar1($c); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + $test->bar2($c); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + $test->prop1 = $c; +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + $test->prop2 = $c; +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + $test->prop3 = $c; +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + $test->prop4 = $c; +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +?> +===DONE=== +--EXPECTF-- +object(A)#2 (0) { +} +object(A)#2 (0) { +} +int(10) +int(10) +object(A)#2 (0) { +} +object(A)#2 (0) { +} +object(B)#3 (0) { +} +object(B)#3 (0) { +} +Test::foo1(): Argument #1 ($v) must be of type (X&Y)|int, C given, called in %s on line %d +Test::foo2(): Argument #1 ($v) must be of type (X&Y)|int, C given, called in %s on line %d +Test::bar1(): Argument #1 ($v) must be of type B|(X&Y), C given, called in %s on line %d +Test::bar2(): Argument #1 ($v) must be of type (X&Y)|B, C given, called in %s on line %d +Cannot assign C to property Test::$prop1 of type (X&Y)|int +Cannot assign C to property Test::$prop2 of type (X&Y)|int +Cannot assign C to property Test::$prop3 of type (X&Y)|B +Cannot assign C to property Test::$prop4 of type B|(X&Y) +===DONE=== diff --git a/Zend/tests/type_declarations/dnf_types/redundant_types/duplicate_class_alias_type.phpt b/Zend/tests/type_declarations/dnf_types/redundant_types/duplicate_class_alias_type.phpt new file mode 100644 index 0000000000000..93ff63856c0b2 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/redundant_types/duplicate_class_alias_type.phpt @@ -0,0 +1,13 @@ +--TEST-- +Duplicate class alias type +--FILE-- + +--EXPECTF-- +Fatal error: Type X&A is redundant with type X&A in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/redundant_types/duplicate_class_alias_type_runtime.phpt b/Zend/tests/type_declarations/dnf_types/redundant_types/duplicate_class_alias_type_runtime.phpt new file mode 100644 index 0000000000000..0b2e2a13d176c --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/redundant_types/duplicate_class_alias_type_runtime.phpt @@ -0,0 +1,15 @@ +--TEST-- +Duplicate class alias type at runtime +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/dnf_types/redundant_types/inheritence.phpt b/Zend/tests/type_declarations/dnf_types/redundant_types/inheritence.phpt new file mode 100644 index 0000000000000..ef57adf09c0e4 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/redundant_types/inheritence.phpt @@ -0,0 +1,15 @@ +--TEST-- +Intersection with child class +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present001.phpt b/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present001.phpt new file mode 100644 index 0000000000000..df0a49a182ba9 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present001.phpt @@ -0,0 +1,14 @@ +--TEST-- +A less restrictive type constrain is part of the DNF type 001 +--FILE-- + +===DONE=== +--EXPECTF-- +Fatal error: Type A is less restrictive than type A&B in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present002.phpt b/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present002.phpt new file mode 100644 index 0000000000000..502df92018f06 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present002.phpt @@ -0,0 +1,14 @@ +--TEST-- +A less restrictive type constrain is part of the DNF type 002 +--FILE-- + +===DONE=== +--EXPECTF-- +Fatal error: Type A is less restrictive than type A&B in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present003.phpt b/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present003.phpt new file mode 100644 index 0000000000000..6351a847d89f6 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present003.phpt @@ -0,0 +1,15 @@ +--TEST-- +A less restrictive type constrain is part of the DNF type 003 +--FILE-- + +===DONE=== +--EXPECTF-- +Fatal error: Type A&B is less restrictive than type A&B&C in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present004.phpt b/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present004.phpt new file mode 100644 index 0000000000000..d8b0991aeb4b9 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/redundant_types/less_restrive_type_constraint_already_present004.phpt @@ -0,0 +1,15 @@ +--TEST-- +A less restrictive type constrain is part of the DNF type 004 +--FILE-- + +===DONE=== +--EXPECTF-- +Fatal error: Type A&B is less restrictive than type A&B&C in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/variance/invalid1.phpt b/Zend/tests/type_declarations/dnf_types/variance/invalid1.phpt new file mode 100644 index 0000000000000..5e90cc7a79801 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/invalid1.phpt @@ -0,0 +1,28 @@ +--TEST-- +Co-variance check failure for intersection type where child replace one of intersection type members with a supertype +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of FooChild::foo(): (A&C)|X must be compatible with Foo::foo(): (B&C)|X in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_drop_type1.phpt b/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_drop_type1.phpt new file mode 100644 index 0000000000000..9aaf44cad636c --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_drop_type1.phpt @@ -0,0 +1,27 @@ +--TEST-- +Co-variance check failure for intersection type where child removes one of intersection type members +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of FooChild::foo(): A|X must be compatible with Foo::foo(): (A&B)|X in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_drop_type2.phpt b/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_drop_type2.phpt new file mode 100644 index 0000000000000..67cd813007cb0 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_drop_type2.phpt @@ -0,0 +1,28 @@ +--TEST-- +Co-variance check failure for intersection type where child removes one of intersection type members +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of FooChild::foo(): (A&B)|X must be compatible with Foo::foo(): (A&B&C)|X in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_intersection_to_union1.phpt b/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_intersection_to_union1.phpt new file mode 100644 index 0000000000000..273bbcf1ac500 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_intersection_to_union1.phpt @@ -0,0 +1,26 @@ +--TEST-- +Co-variance failure for intersection type where child is union, but not all members are a subtype of intersection 1 +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of B::foo(): TestOne|TestTwo must be compatible with A::foo(): (X&Y)|L in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_intersection_to_union2.phpt b/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_intersection_to_union2.phpt new file mode 100644 index 0000000000000..1eafcb5d58718 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_intersection_to_union2.phpt @@ -0,0 +1,23 @@ +--TEST-- +Co-variance failure for intersection type where child is union, but not all members are a subtype of intersection 2 +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of B::foo(): TestOne|int must be compatible with A::foo(): X&Y in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_intersection_to_union3.phpt b/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_intersection_to_union3.phpt new file mode 100644 index 0000000000000..c4dd3449f976d --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/invalid_covariance_intersection_to_union3.phpt @@ -0,0 +1,25 @@ +--TEST-- +Co-variance failure for intersection type where child is union, but not all members are a subtype of intersection 2 +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of B::foo(): TestOne|TestTwo must be compatible with A::foo(): X&Z in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/variance/invalid_invariance1.phpt b/Zend/tests/type_declarations/dnf_types/variance/invalid_invariance1.phpt new file mode 100644 index 0000000000000..65d3d2033bbc1 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/invalid_invariance1.phpt @@ -0,0 +1,18 @@ +--TEST-- +Property types must be invariant +--FILE-- + +--EXPECTF-- +Fatal error: Type of B::$prop must be (X&Y)|L (as in class A) in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/variance/invalid_invariance2.phpt b/Zend/tests/type_declarations/dnf_types/variance/invalid_invariance2.phpt new file mode 100644 index 0000000000000..3b82afa89355e --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/invalid_invariance2.phpt @@ -0,0 +1,21 @@ +--TEST-- +Intersection type reduction invalid invariant type check +--FILE-- + +===DONE=== +--EXPECTF-- +Fatal error: Type of Test2::$prop must be (X&Y)|B (as in class Test) in %s on line %d diff --git a/Zend/tests/type_declarations/dnf_types/variance/valid1.phpt b/Zend/tests/type_declarations/dnf_types/variance/valid1.phpt new file mode 100644 index 0000000000000..3282b0f9dc04f --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/valid1.phpt @@ -0,0 +1,40 @@ +--TEST-- +Valid inheritence - co-variance +--FILE-- +foo()); +$o = new FooSecondChild(); +var_dump($o->foo()); + +?> +--EXPECTF-- +object(Test)#%d (0) { +} +object(Test)#%d (0) { +} diff --git a/Zend/tests/type_declarations/dnf_types/variance/valid2.phpt b/Zend/tests/type_declarations/dnf_types/variance/valid2.phpt new file mode 100644 index 0000000000000..f77380326db96 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/valid2.phpt @@ -0,0 +1,23 @@ +--TEST-- +Commutative intersection types +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/dnf_types/variance/valid4.phpt b/Zend/tests/type_declarations/dnf_types/variance/valid4.phpt new file mode 100644 index 0000000000000..78691715c8e59 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/valid4.phpt @@ -0,0 +1,20 @@ +--TEST-- +Intersection type reduction valid invariant type check +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/dnf_types/variance/valid5.phpt b/Zend/tests/type_declarations/dnf_types/variance/valid5.phpt new file mode 100644 index 0000000000000..ca75aeb584f00 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/valid5.phpt @@ -0,0 +1,46 @@ +--TEST-- +Replacing union of classes respecting intersection type by intersection type +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/dnf_types/variance/valid6.phpt b/Zend/tests/type_declarations/dnf_types/variance/valid6.phpt new file mode 100644 index 0000000000000..faf63748aec34 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/valid6.phpt @@ -0,0 +1,20 @@ +--TEST-- +Replacing union type by intersection type +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/dnf_types/variance/valid7.phpt b/Zend/tests/type_declarations/dnf_types/variance/valid7.phpt new file mode 100644 index 0000000000000..3d9a58638e2f4 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/valid7.phpt @@ -0,0 +1,23 @@ +--TEST-- +Replacing object type with intersection type +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/dnf_types/variance/valid8.phpt b/Zend/tests/type_declarations/dnf_types/variance/valid8.phpt new file mode 100644 index 0000000000000..08628dc83f586 --- /dev/null +++ b/Zend/tests/type_declarations/dnf_types/variance/valid8.phpt @@ -0,0 +1,20 @@ +--TEST-- +Replacing iterable type with intersection type in DUT +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_iterable_type.phpt b/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_iterable_type.phpt index fc4ee2d5607d0..3fd321d394ba4 100644 --- a/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_iterable_type.phpt +++ b/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_iterable_type.phpt @@ -7,4 +7,4 @@ function foo(): iterable&Iterator {} ?> --EXPECTF-- -Fatal error: Type iterable cannot be part of an intersection type in %s on line %d +Fatal error: Type Traversable|array cannot be part of an intersection type in %s on line %d diff --git a/Zend/tests/type_declarations/intersection_types/variance/invalid5.phpt b/Zend/tests/type_declarations/intersection_types/variance/invalid5.phpt index b704f89b909f2..ce3a20fb03ad5 100644 --- a/Zend/tests/type_declarations/intersection_types/variance/invalid5.phpt +++ b/Zend/tests/type_declarations/intersection_types/variance/invalid5.phpt @@ -15,4 +15,4 @@ class Test2 extends Test { ?> --EXPECTF-- -Fatal error: Declaration of Test2::method(): X&Y must be compatible with Test::method(): iterable in %s on line %d +Fatal error: Declaration of Test2::method(): X&Y must be compatible with Test::method(): Traversable|array in %s on line %d diff --git a/Zend/tests/type_declarations/iterable_001.phpt b/Zend/tests/type_declarations/iterable/iterable_001.phpt similarity index 84% rename from Zend/tests/type_declarations/iterable_001.phpt rename to Zend/tests/type_declarations/iterable/iterable_001.phpt index 10a001dea0253..f75418009086e 100644 --- a/Zend/tests/type_declarations/iterable_001.phpt +++ b/Zend/tests/type_declarations/iterable/iterable_001.phpt @@ -45,4 +45,4 @@ object(ArrayIterator)#1 (1) { int(3) } } -test(): Argument #1 ($iterable) must be of type iterable, int given, called in %s on line %d +test(): Argument #1 ($iterable) must be of type Traversable|array, int given, called in %s on line %d diff --git a/Zend/tests/type_declarations/iterable_002.phpt b/Zend/tests/type_declarations/iterable/iterable_002.phpt similarity index 89% rename from Zend/tests/type_declarations/iterable_002.phpt rename to Zend/tests/type_declarations/iterable/iterable_002.phpt index fdc3b20df5850..d1e1b09d187ac 100644 --- a/Zend/tests/type_declarations/iterable_002.phpt +++ b/Zend/tests/type_declarations/iterable/iterable_002.phpt @@ -17,4 +17,4 @@ function baz(iterable $iterable = 1) { ?> --EXPECTF-- -Fatal error: Cannot use int as default value for parameter $iterable of type iterable in %s on line %d +Fatal error: Cannot use int as default value for parameter $iterable of type Traversable|array in %s on line %d diff --git a/Zend/tests/type_declarations/iterable_003.phpt b/Zend/tests/type_declarations/iterable/iterable_003.phpt similarity index 84% rename from Zend/tests/type_declarations/iterable_003.phpt rename to Zend/tests/type_declarations/iterable/iterable_003.phpt index d2a3bcb368cfd..d7c5b206ebaca 100644 --- a/Zend/tests/type_declarations/iterable_003.phpt +++ b/Zend/tests/type_declarations/iterable/iterable_003.phpt @@ -29,4 +29,4 @@ array(0) { } object(Generator)#2 (0) { } -baz(): Return value must be of type iterable, int returned +baz(): Return value must be of type Traversable|array, int returned diff --git a/Zend/tests/type_declarations/iterable_004.phpt b/Zend/tests/type_declarations/iterable/iterable_004.phpt similarity index 74% rename from Zend/tests/type_declarations/iterable_004.phpt rename to Zend/tests/type_declarations/iterable/iterable_004.phpt index fe9d4461b98b7..8b54482625a27 100644 --- a/Zend/tests/type_declarations/iterable_004.phpt +++ b/Zend/tests/type_declarations/iterable/iterable_004.phpt @@ -21,4 +21,4 @@ class Bar extends Foo { ?> --EXPECTF-- -Fatal error: Declaration of Bar::testScalar(iterable $iterable) must be compatible with Foo::testScalar(int $int) in %s on line %d +Fatal error: Declaration of Bar::testScalar(Traversable|array $iterable) must be compatible with Foo::testScalar(int $int) in %s on line %d diff --git a/Zend/tests/type_declarations/iterable_005.phpt b/Zend/tests/type_declarations/iterable/iterable_005.phpt similarity index 88% rename from Zend/tests/type_declarations/iterable_005.phpt rename to Zend/tests/type_declarations/iterable/iterable_005.phpt index 39dede3b5c31a..2f5fb83f07d7f 100644 --- a/Zend/tests/type_declarations/iterable_005.phpt +++ b/Zend/tests/type_declarations/iterable/iterable_005.phpt @@ -29,4 +29,4 @@ class TestScalar extends Test { ?> --EXPECTF-- -Fatal error: Declaration of TestScalar::method(): int must be compatible with Test::method(): iterable in %s on line %d +Fatal error: Declaration of TestScalar::method(): int must be compatible with Test::method(): Traversable|array in %s on line %d diff --git a/Zend/tests/type_declarations/iterable/or_null.phpt b/Zend/tests/type_declarations/iterable/or_null.phpt new file mode 100644 index 0000000000000..286f1e291db71 --- /dev/null +++ b/Zend/tests/type_declarations/iterable/or_null.phpt @@ -0,0 +1,46 @@ +--TEST-- +Test "or null"/"or be null" in type-checking errors for userland functions with iterable +--FILE-- + +--EXPECTF-- +TypeError: iterableF(): Argument #1 ($param) must be of type Traversable|array|null, int given, called in %s on line %d and defined in %s:%d +Stack trace: +#0 %s(%d): iterableF(1) +#1 {main} +TypeError: returnIterable(): Return value must be of type Traversable|array|null, int returned in %s:%d +Stack trace: +#0 %s(%d): returnIterable() +#1 {main} +TypeError: returnMissingIterable(): Return value must be of type Traversable|array|null, none returned in %s:%d +Stack trace: +#0 %s(%d): returnMissingIterable() +#1 {main} diff --git a/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_Traversable.phpt b/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_Traversable.phpt index 5b65a33de1a59..f8d9287be6be7 100644 --- a/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_Traversable.phpt +++ b/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_Traversable.phpt @@ -8,4 +8,4 @@ function test(): iterable|Traversable { ?> --EXPECTF-- -Fatal error: Type Traversable|iterable contains both iterable and Traversable, which is redundant in %s on line %d +Fatal error: Duplicate type Traversable is redundant in %s on line %d diff --git a/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_Traversable_2.phpt b/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_Traversable_2.phpt index e3f7c5858b400..a36d73e3f3083 100644 --- a/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_Traversable_2.phpt +++ b/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_Traversable_2.phpt @@ -8,4 +8,4 @@ function test(): iterable|Traversable|ArrayAccess { ?> --EXPECTF-- -Fatal error: Type Traversable|ArrayAccess|iterable contains both iterable and Traversable, which is redundant in %s on line %d +Fatal error: Duplicate type Traversable is redundant in %s on line %d diff --git a/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_array.phpt b/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_array.phpt index c6b0949418890..ca75ff705edf7 100644 --- a/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_array.phpt +++ b/Zend/tests/type_declarations/union_types/redundant_types/iterable_and_array.phpt @@ -8,4 +8,4 @@ function test(): iterable|array { ?> --EXPECTF-- -Fatal error: Type iterable|array contains both iterable and array, which is redundant in %s on line %d +Fatal error: Duplicate type array is redundant in %s on line %d diff --git a/Zend/tests/typehints/or_null.phpt b/Zend/tests/typehints/or_null.phpt index d7a2e23002690..279a04aaff102 100644 --- a/Zend/tests/typehints/or_null.phpt +++ b/Zend/tests/typehints/or_null.phpt @@ -57,14 +57,6 @@ try { echo $e, PHP_EOL; } -function iterableF(?iterable $param) {} - -try { - iterableF(1); -} catch (\TypeError $e) { - echo $e, PHP_EOL; -} - function intF(?int $param) {} try { @@ -143,16 +135,6 @@ try { echo $e, PHP_EOL; } -function returnIterable(): ?iterable { - return 1; -} - -try { - returnIterable(); -} catch (\TypeError $e) { - echo $e, PHP_EOL; -} - function returnInt(): ?int { return new \StdClass; } @@ -199,15 +181,6 @@ try { echo $e, PHP_EOL; } -function returnMissingIterable(): ?iterable { -} - -try { - returnMissingIterable(); -} catch (\TypeError $e) { - echo $e, PHP_EOL; -} - function returnMissingInt(): ?int { } @@ -221,97 +194,85 @@ try { --EXPECTF-- TypeError: unloadedClass(): Argument #1 ($param) must be of type ?I\Dont\Exist, stdClass given, called in %s:%d Stack trace: -#0 %s(8): unloadedClass(Object(stdClass)) +#0 %s(%d): unloadedClass(Object(stdClass)) #1 {main} TypeError: loadedClass(): Argument #1 ($param) must be of type ?RealClass, stdClass given, called in %s:%d Stack trace: -#0 %s(20): loadedClass(Object(stdClass)) +#0 %s(%d): loadedClass(Object(stdClass)) #1 {main} TypeError: loadedInterface(): Argument #1 ($param) must be of type ?RealInterface, stdClass given, called in %s:%d Stack trace: -#0 %s(26): loadedInterface(Object(stdClass)) +#0 %s(%d): loadedInterface(Object(stdClass)) #1 {main} TypeError: unloadedClass(): Argument #1 ($param) must be of type ?I\Dont\Exist, int given, called in %s:%d Stack trace: -#0 %s(32): unloadedClass(1) +#0 %s(%d): unloadedClass(1) #1 {main} TypeError: loadedClass(): Argument #1 ($param) must be of type ?RealClass, int given, called in %s:%d Stack trace: -#0 %s(38): loadedClass(1) +#0 %s(%d): loadedClass(1) #1 {main} TypeError: loadedInterface(): Argument #1 ($param) must be of type ?RealInterface, int given, called in %s:%d Stack trace: -#0 %s(44): loadedInterface(1) +#0 %s(%d): loadedInterface(1) #1 {main} TypeError: callableF(): Argument #1 ($param) must be of type ?callable, int given, called in %s:%d Stack trace: -#0 %s(52): callableF(1) -#1 {main} -TypeError: iterableF(): Argument #1 ($param) must be of type ?iterable, int given, called in %s:%d -Stack trace: -#0 %s(60): iterableF(1) +#0 %s(%d): callableF(1) #1 {main} TypeError: intF(): Argument #1 ($param) must be of type ?int, stdClass given, called in %s:%d Stack trace: -#0 %s(68): intF(Object(stdClass)) +#0 %s(%d): intF(Object(stdClass)) #1 {main} TypeError: returnUnloadedClass(): Return value must be of type ?I\Dont\Exist, stdClass returned in %s:%d Stack trace: -#0 %s(78): returnUnloadedClass() +#0 %s(%d): returnUnloadedClass() #1 {main} TypeError: returnLoadedClass(): Return value must be of type ?RealClass, stdClass returned in %s:%d Stack trace: -#0 %s(88): returnLoadedClass() +#0 %s(%d): returnLoadedClass() #1 {main} TypeError: returnLoadedInterface(): Return value must be of type ?RealInterface, stdClass returned in %s:%d Stack trace: -#0 %s(98): returnLoadedInterface() +#0 %s(%d): returnLoadedInterface() #1 {main} TypeError: returnUnloadedClassScalar(): Return value must be of type ?I\Dont\Exist, int returned in %s:%d Stack trace: -#0 %s(108): returnUnloadedClassScalar() +#0 %s(%d): returnUnloadedClassScalar() #1 {main} TypeError: returnLoadedClassScalar(): Return value must be of type ?RealClass, int returned in %s:%d Stack trace: -#0 %s(118): returnLoadedClassScalar() +#0 %s(%d): returnLoadedClassScalar() #1 {main} TypeError: returnLoadedInterfaceScalar(): Return value must be of type ?RealInterface, int returned in %s:%d Stack trace: -#0 %s(128): returnLoadedInterfaceScalar() +#0 %s(%d): returnLoadedInterfaceScalar() #1 {main} TypeError: returnCallable(): Return value must be of type ?callable, int returned in %s:%d Stack trace: -#0 %s(138): returnCallable() -#1 {main} -TypeError: returnIterable(): Return value must be of type ?iterable, int returned in %s:%d -Stack trace: -#0 %s(148): returnIterable() +#0 %s(%d): returnCallable() #1 {main} TypeError: returnInt(): Return value must be of type ?int, stdClass returned in %s:%d Stack trace: -#0 %s(158): returnInt() +#0 %s(%d): returnInt() #1 {main} TypeError: returnMissingUnloadedClass(): Return value must be of type ?I\Dont\Exist, none returned in %s:%d Stack trace: -#0 %s(167): returnMissingUnloadedClass() +#0 %s(%d): returnMissingUnloadedClass() #1 {main} TypeError: returnMissingLoadedClass(): Return value must be of type ?RealClass, none returned in %s:%d Stack trace: -#0 %s(176): returnMissingLoadedClass() +#0 %s(%d): returnMissingLoadedClass() #1 {main} TypeError: returnMissingLoadedInterface(): Return value must be of type ?RealInterface, none returned in %s:%d Stack trace: -#0 %s(185): returnMissingLoadedInterface() +#0 %s(%d): returnMissingLoadedInterface() #1 {main} TypeError: returnMissingCallable(): Return value must be of type ?callable, none returned in %s:%d Stack trace: -#0 %s(194): returnMissingCallable() -#1 {main} -TypeError: returnMissingIterable(): Return value must be of type ?iterable, none returned in %s:%d -Stack trace: -#0 %s(203): returnMissingIterable() +#0 %s(%d): returnMissingCallable() #1 {main} TypeError: returnMissingInt(): Return value must be of type ?int, none returned in %s:%d Stack trace: -#0 %s(212): returnMissingInt() +#0 %s(%d): returnMissingInt() #1 {main} diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 9e903f16ce1ef..6d02703736210 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -2805,6 +2805,7 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend } } + /* Rebuild arginfos if parameter/property types and/or a return type are used */ if (reg_function->common.arg_info && (reg_function->common.fn_flags & (ZEND_ACC_HAS_RETURN_TYPE|ZEND_ACC_HAS_TYPE_HINTS))) { /* convert "const char*" class type names into "zend_string*" */ @@ -2856,6 +2857,15 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend } } } + if (ZEND_TYPE_IS_ITERABLE_FALLBACK(new_arg_info[i].type)) { + /* Warning generated an extension load warning which is emitted for every test + zend_error(E_CORE_WARNING, "iterable type is now a compile time alias for array|Traversable," + " regenerate the argument info via the php-src gen_stub build script"); + */ + zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_CONST_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE), + (new_arg_info[i].type.type_mask|MAY_BE_ARRAY)); + new_arg_info[i].type = legacy_iterable; + } } } diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 6c06a0a01b932..7a3dfdedc0cb7 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1191,16 +1191,50 @@ static zend_string *resolve_class_name(zend_string *name, zend_class_entry *scop return zend_string_copy(name); } +static zend_string *add_intersection_type(zend_string *str, + zend_type_list *intersection_type_list, zend_class_entry *scope, + bool is_bracketed) +{ + zend_type *single_type; + zend_string *intersection_str = NULL; + + ZEND_TYPE_LIST_FOREACH(intersection_type_list, single_type) { + zend_string *name = ZEND_TYPE_NAME(*single_type); + zend_string *resolved = resolve_class_name(name, scope); + intersection_str = add_type_string(intersection_str, resolved, /* is_intersection */ true); + zend_string_release(resolved); + } ZEND_TYPE_LIST_FOREACH_END(); + + if (is_bracketed) { + zend_string *result = zend_string_concat3("(", 1, ZSTR_VAL(intersection_str), ZSTR_LEN(intersection_str), ")", 1); + zend_string_release(intersection_str); + intersection_str = result; + } + str = add_type_string(str, intersection_str, /* is_intersection */ false); + zend_string_release(intersection_str); + return str; +} + zend_string *zend_type_to_string_resolved(zend_type type, zend_class_entry *scope) { zend_string *str = NULL; - if (ZEND_TYPE_HAS_LIST(type)) { + /* Pure intersection type */ + if (ZEND_TYPE_IS_INTERSECTION(type)) { + ZEND_ASSERT(!ZEND_TYPE_IS_UNION(type)); + str = add_intersection_type(str, ZEND_TYPE_LIST(type), scope, /* is_bracketed */ false); + } else if (ZEND_TYPE_HAS_LIST(type)) { + /* A union type might not be a list */ zend_type *list_type; - bool is_intersection = ZEND_TYPE_IS_INTERSECTION(type); ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), list_type) { + if (ZEND_TYPE_IS_INTERSECTION(*list_type)) { + str = add_intersection_type(str, ZEND_TYPE_LIST(*list_type), scope, /* is_bracketed */ true); + continue; + } + ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*list_type)); + ZEND_ASSERT(ZEND_TYPE_HAS_NAME(*list_type)); zend_string *name = ZEND_TYPE_NAME(*list_type); zend_string *resolved = resolve_class_name(name, scope); - str = add_type_string(str, resolved, is_intersection); + str = add_type_string(str, resolved, /* is_intersection */ false); zend_string_release(resolved); } ZEND_TYPE_LIST_FOREACH_END(); } else if (ZEND_TYPE_HAS_NAME(type)) { @@ -1227,9 +1261,6 @@ zend_string *zend_type_to_string_resolved(zend_type type, zend_class_entry *scop if (type_mask & MAY_BE_CALLABLE) { str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_CALLABLE), /* is_intersection */ false); } - if (type_mask & MAY_BE_ITERABLE) { - str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_ITERABLE), /* is_intersection */ false); - } if (type_mask & MAY_BE_OBJECT) { str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_OBJECT), /* is_intersection */ false); } @@ -1259,7 +1290,8 @@ zend_string *zend_type_to_string_resolved(zend_type type, zend_class_entry *scop if (type_mask & MAY_BE_NULL) { bool is_union = !str || memchr(ZSTR_VAL(str), '|', ZSTR_LEN(str)) != NULL; - if (!is_union) { + bool has_intersection = !str || memchr(ZSTR_VAL(str), '&', ZSTR_LEN(str)) != NULL; + if (!is_union && !has_intersection) { zend_string *nullable_str = zend_string_concat2("?", 1, ZSTR_VAL(str), ZSTR_LEN(str)); zend_string_release(str); return nullable_str; @@ -1289,7 +1321,7 @@ static void zend_mark_function_as_generator(void) /* {{{ */ if (CG(active_op_array)->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { zend_type return_type = CG(active_op_array)->arg_info[-1].type; - bool valid_type = (ZEND_TYPE_FULL_MASK(return_type) & (MAY_BE_ITERABLE | MAY_BE_OBJECT)) != 0; + bool valid_type = (ZEND_TYPE_FULL_MASK(return_type) & MAY_BE_OBJECT) != 0; if (!valid_type) { zend_type *single_type; ZEND_TYPE_FOREACH(return_type, single_type) { @@ -6134,6 +6166,7 @@ static zend_type zend_compile_single_typename(zend_ast *ast) zend_error_noreturn(E_COMPILE_ERROR, "Cannot use \"static\" when no class scope is active"); } + return (zend_type) ZEND_TYPE_INIT_CODE(ast->attr, 0, 0); } else { zend_string *class_name = zend_ast_get_str(ast); @@ -6145,6 +6178,15 @@ static zend_type zend_compile_single_typename(zend_ast *ast) "Type declaration '%s' must be unqualified", ZSTR_VAL(zend_string_tolower(class_name))); } + + /* Transform iterable into a type union alias */ + if (type_code == IS_ITERABLE) { + /* Set iterable bit for BC compat during Reflection and string representation of type */ + zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_CONST_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE), + (MAY_BE_ARRAY|_ZEND_TYPE_ITERABLE_BIT)); + return iterable; + } + return (zend_type) ZEND_TYPE_INIT_CODE(type_code, 0, 0); } else { const char *correct_name; @@ -6184,19 +6226,91 @@ static zend_type zend_compile_single_typename(zend_ast *ast) } } -static bool zend_type_contains_traversable(zend_type type) { - zend_type *single_type; - ZEND_TYPE_FOREACH(type, single_type) { - if (ZEND_TYPE_HAS_NAME(*single_type) - && zend_string_equals_literal_ci(ZEND_TYPE_NAME(*single_type), "Traversable")) { - return 1; +static void zend_are_intersection_types_redundant(zend_type left_type, zend_type right_type) +{ + ZEND_ASSERT(ZEND_TYPE_IS_INTERSECTION(left_type)); + ZEND_ASSERT(ZEND_TYPE_IS_INTERSECTION(right_type)); + zend_type_list *l_type_list = ZEND_TYPE_LIST(left_type); + zend_type_list *r_type_list = ZEND_TYPE_LIST(right_type); + zend_type_list *smaller_type_list, *larger_type_list; + bool flipped = false; + + if (r_type_list->num_types < l_type_list->num_types) { + smaller_type_list = r_type_list; + larger_type_list = l_type_list; + flipped = true; + } else { + smaller_type_list = l_type_list; + larger_type_list = r_type_list; + } + + int sum = 0; + zend_type *outer_type; + ZEND_TYPE_LIST_FOREACH(smaller_type_list, outer_type) + zend_type *inner_type; + ZEND_TYPE_LIST_FOREACH(larger_type_list, inner_type) + if (zend_string_equals_ci(ZEND_TYPE_NAME(*inner_type), ZEND_TYPE_NAME(*outer_type))) { + sum++; + break; + } + ZEND_TYPE_LIST_FOREACH_END(); + ZEND_TYPE_LIST_FOREACH_END(); + + if (sum == smaller_type_list->num_types) { + zend_string *l_type_str = zend_type_to_string(left_type); + zend_string *r_type_str = zend_type_to_string(right_type); + if (smaller_type_list->num_types == larger_type_list->num_types) { + if (flipped) { + zend_error_noreturn(E_COMPILE_ERROR, "Type %s is redundant with type %s", + ZSTR_VAL(r_type_str), ZSTR_VAL(l_type_str)); + } else { + zend_error_noreturn(E_COMPILE_ERROR, "Type %s is redundant with type %s", + ZSTR_VAL(l_type_str), ZSTR_VAL(r_type_str)); + } + } else { + if (flipped) { + zend_error_noreturn(E_COMPILE_ERROR, "Type %s is less restrictive than type %s", + ZSTR_VAL(r_type_str), ZSTR_VAL(l_type_str)); + } else { + zend_error_noreturn(E_COMPILE_ERROR, "Type %s is less restrictive than type %s", + ZSTR_VAL(l_type_str), ZSTR_VAL(r_type_str)); + } } - } ZEND_TYPE_FOREACH_END(); - return 0; + } +} + +static void zend_is_intersection_type_redundant_by_single_type(zend_type intersection_type, zend_type single_type) +{ + ZEND_ASSERT(ZEND_TYPE_IS_INTERSECTION(intersection_type)); + ZEND_ASSERT(!ZEND_TYPE_IS_INTERSECTION(single_type)); + + zend_type *single_intersection_type = NULL; + ZEND_TYPE_FOREACH(intersection_type, single_intersection_type) + if (zend_string_equals_ci(ZEND_TYPE_NAME(*single_intersection_type), ZEND_TYPE_NAME(single_type))) { + zend_string *single_type_str = zend_type_to_string(single_type); + zend_string *complete_type = zend_type_to_string(intersection_type); + zend_error_noreturn(E_COMPILE_ERROR, "Type %s is less restrictive than type %s", + ZSTR_VAL(single_type_str), ZSTR_VAL(complete_type)); + } + ZEND_TYPE_FOREACH_END(); +} + +/* Used by both intersection and union types prior to transforming the type list to a full zend_type */ +static void zend_is_type_list_redundant_by_single_type(zend_type_list *type_list, zend_type type) +{ + ZEND_ASSERT(!ZEND_TYPE_IS_INTERSECTION(type)); + for (size_t i = 0; i < type_list->num_types - 1; i++) { + if (ZEND_TYPE_IS_INTERSECTION(type_list->types[i])) { + zend_is_intersection_type_redundant_by_single_type(type_list->types[i], type); + continue; + } + if (zend_string_equals_ci(ZEND_TYPE_NAME(type_list->types[i]), ZEND_TYPE_NAME(type))) { + zend_string *single_type_str = zend_type_to_string(type); + zend_error_noreturn(E_COMPILE_ERROR, "Duplicate type %s is redundant", ZSTR_VAL(single_type_str)); + } + } } -// TODO: Ideally we'd canonicalize "iterable" into "array|Traversable" and essentially -// treat it as a built-in type alias. static zend_type zend_compile_typename( zend_ast *ast, bool force_allow_null) /* {{{ */ { @@ -6211,6 +6325,7 @@ static zend_type zend_compile_typename( if (ast->kind == ZEND_AST_TYPE_UNION) { zend_ast_list *list = zend_ast_get_list(ast); zend_type_list *type_list; + bool is_composite = false; ALLOCA_FLAG(use_heap) type_list = do_alloca(ZEND_TYPE_LIST_SIZE(list->children), use_heap); @@ -6218,7 +6333,37 @@ static zend_type zend_compile_typename( for (uint32_t i = 0; i < list->children; i++) { zend_ast *type_ast = list->child[i]; - zend_type single_type = zend_compile_single_typename(type_ast); + zend_type single_type; + + if (type_ast->kind == ZEND_AST_TYPE_INTERSECTION) { + is_composite = true; + /* The first class type can be stored directly as the type ptr payload. */ + if (ZEND_TYPE_IS_COMPLEX(type) && !ZEND_TYPE_HAS_LIST(type)) { + /* Switch from single name to name list. */ + type_list->num_types = 1; + type_list->types[0] = type; + ZEND_TYPE_FULL_MASK(type_list->types[0]) &= ~_ZEND_TYPE_MAY_BE_MASK; + ZEND_TYPE_SET_LIST(type, type_list); + } + + single_type = zend_compile_typename(type_ast, false); + ZEND_ASSERT(ZEND_TYPE_IS_INTERSECTION(single_type)); + + type_list->types[type_list->num_types++] = single_type; + + /* Check for trivially redundant class types */ + for (size_t i = 0; i < type_list->num_types - 1; i++) { + if (ZEND_TYPE_IS_INTERSECTION(type_list->types[i])) { + zend_are_intersection_types_redundant(single_type, type_list->types[i]); + continue; + } + /* Type from type list is a simple type */ + zend_is_intersection_type_redundant_by_single_type(single_type, type_list->types[i]); + } + continue; + } + + single_type = zend_compile_single_typename(type_ast); uint32_t single_type_mask = ZEND_TYPE_PURE_MASK(single_type); if (single_type_mask == MAY_BE_ANY) { @@ -6236,7 +6381,7 @@ static zend_type zend_compile_typename( ZEND_TYPE_FULL_MASK(single_type) &= ~_ZEND_TYPE_MAY_BE_MASK; if (ZEND_TYPE_IS_COMPLEX(single_type)) { - if (!ZEND_TYPE_IS_COMPLEX(type)) { + if (!ZEND_TYPE_IS_COMPLEX(type) && !is_composite) { /* The first class type can be stored directly as the type ptr payload. */ ZEND_TYPE_SET_PTR(type, ZEND_TYPE_NAME(single_type)); ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_NAME_BIT; @@ -6252,14 +6397,7 @@ static zend_type zend_compile_typename( type_list->types[type_list->num_types++] = single_type; /* Check for trivially redundant class types */ - for (size_t i = 0; i < type_list->num_types - 1; i++) { - if (zend_string_equals_ci( - ZEND_TYPE_NAME(type_list->types[i]), ZEND_TYPE_NAME(single_type))) { - zend_string *single_type_str = zend_type_to_string(single_type); - zend_error_noreturn(E_COMPILE_ERROR, - "Duplicate type %s is redundant", ZSTR_VAL(single_type_str)); - } - } + zend_is_type_list_redundant_by_single_type(type_list, single_type); } } } @@ -6290,6 +6428,14 @@ static zend_type zend_compile_typename( zend_ast *type_ast = list->child[i]; zend_type single_type = zend_compile_single_typename(type_ast); + /* An intersection of union types cannot exist so invalidate it + * Currently only can happen with iterable getting canonicalized to Traversable|array */ + if (ZEND_TYPE_IS_ITERABLE_FALLBACK(single_type)) { + zend_string *standard_type_str = zend_type_to_string(single_type); + zend_error_noreturn(E_COMPILE_ERROR, + "Type %s cannot be part of an intersection type", ZSTR_VAL(standard_type_str)); + zend_string_release_ex(standard_type_str, false); + } /* An intersection of standard types cannot exist so invalidate it */ if (ZEND_TYPE_IS_ONLY_MASK(single_type)) { zend_string *standard_type_str = zend_type_to_string(single_type); @@ -6308,14 +6454,7 @@ static zend_type zend_compile_typename( type_list->types[type_list->num_types++] = single_type; /* Check for trivially redundant class types */ - for (size_t i = 0; i < type_list->num_types - 1; i++) { - if (zend_string_equals_ci( - ZEND_TYPE_NAME(type_list->types[i]), ZEND_TYPE_NAME(single_type))) { - zend_string *single_type_str = zend_type_to_string(single_type); - zend_error_noreturn(E_COMPILE_ERROR, - "Duplicate type %s is redundant", ZSTR_VAL(single_type_str)); - } - } + zend_is_type_list_redundant_by_single_type(type_list, single_type); } ZEND_ASSERT(list->children == type_list->num_types); @@ -6329,18 +6468,6 @@ static zend_type zend_compile_typename( } uint32_t type_mask = ZEND_TYPE_PURE_MASK(type); - if ((type_mask & (MAY_BE_ARRAY|MAY_BE_ITERABLE)) == (MAY_BE_ARRAY|MAY_BE_ITERABLE)) { - zend_string *type_str = zend_type_to_string(type); - zend_error_noreturn(E_COMPILE_ERROR, - "Type %s contains both iterable and array, which is redundant", ZSTR_VAL(type_str)); - } - - if ((type_mask & MAY_BE_ITERABLE) && zend_type_contains_traversable(type)) { - zend_string *type_str = zend_type_to_string(type); - zend_error_noreturn(E_COMPILE_ERROR, - "Type %s contains both iterable and Traversable, which is redundant", - ZSTR_VAL(type_str)); - } if (type_mask == MAY_BE_ANY && is_marked_nullable) { zend_error_noreturn(E_COMPILE_ERROR, "Type mixed cannot be marked as nullable since mixed already includes null"); @@ -6387,9 +6514,6 @@ static bool zend_is_valid_default_value(zend_type type, zval *value) convert_to_double(value); return 1; } - if ((ZEND_TYPE_FULL_MASK(type) & MAY_BE_ITERABLE) && Z_TYPE_P(value) == IS_ARRAY) { - return 1; - } return 0; } @@ -6644,12 +6768,12 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 } if (force_nullable && ZEND_TYPE_IS_INTERSECTION(arg_info->type)) { + /* We drop the nullable type flag to generate the correct type string */ + ZEND_TYPE_FULL_MASK(arg_info->type) = ZEND_TYPE_FULL_MASK(arg_info->type) & ~MAY_BE_NULL; zend_string *type_str = zend_type_to_string(arg_info->type); zend_error_noreturn(E_COMPILE_ERROR, "Cannot use null as default value for parameter $%s of type %s", - /* We move type_str pointer one char forward to skip the '?' generated by - * the call to zend_compile_typename() */ - ZSTR_VAL(name), ZSTR_VAL(type_str)+1); + ZSTR_VAL(name), ZSTR_VAL(type_str)); } if (default_type != IS_UNDEF && default_type != IS_CONSTANT_AST && !force_nullable diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 7b55fa5e8ac21..f037adb37199b 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -868,20 +868,38 @@ static zend_always_inline zend_class_entry *zend_ce_from_type( return resolve_single_class_type(name, info->ce); } +static bool zend_check_intersection_for_property_class_type(zend_type_list *intersection_type_list, + zend_property_info *info, zend_class_entry *object_ce) +{ + zend_type *list_type; + + ZEND_TYPE_LIST_FOREACH(intersection_type_list, list_type) { + ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*list_type)); + zend_class_entry *ce = zend_ce_from_type(info, list_type); + if (!ce || !instanceof_function(object_ce, ce)) { + return false; + } + } ZEND_TYPE_LIST_FOREACH_END(); + return true; +} + static bool zend_check_and_resolve_property_class_type( zend_property_info *info, zend_class_entry *object_ce) { if (ZEND_TYPE_HAS_LIST(info->type)) { zend_type *list_type; if (ZEND_TYPE_IS_INTERSECTION(info->type)) { - ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(info->type), list_type) { - zend_class_entry *ce = zend_ce_from_type(info, list_type); - if (!ce || !instanceof_function(object_ce, ce)) { - return false; - } - } ZEND_TYPE_LIST_FOREACH_END(); - return true; + return zend_check_intersection_for_property_class_type( + ZEND_TYPE_LIST(info->type), info, object_ce); } else { ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(info->type), list_type) { + if (ZEND_TYPE_IS_INTERSECTION(*list_type)) { + if (zend_check_intersection_for_property_class_type( + ZEND_TYPE_LIST(*list_type), info, object_ce)) { + return true; + } + continue; + } + ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*list_type)); zend_class_entry *ce = zend_ce_from_type(info, list_type); if (ce && instanceof_function(object_ce, ce)) { return true; @@ -909,9 +927,6 @@ static zend_always_inline bool i_zend_check_property_type(zend_property_info *in uint32_t type_mask = ZEND_TYPE_FULL_MASK(info->type); ZEND_ASSERT(!(type_mask & (MAY_BE_CALLABLE|MAY_BE_STATIC))); - if ((type_mask & MAY_BE_ITERABLE) && zend_is_iterable(property)) { - return 1; - } return zend_verify_scalar_type_hint(type_mask, property, strict, 0); } @@ -1000,6 +1015,22 @@ static zend_always_inline zend_class_entry *zend_fetch_ce_from_cache_slot( return ce; } +static bool zend_check_intersection_type_from_cache_slot(zend_type_list *intersection_type_list, + zend_class_entry *arg_ce, void **cache_slot) +{ + zend_class_entry *ce; + zend_type *list_type; + ZEND_TYPE_LIST_FOREACH(intersection_type_list, list_type) { + ce = zend_fetch_ce_from_cache_slot(cache_slot, list_type); + /* If type is not an instance of one of the types taking part in the + * intersection it cannot be a valid instance of the whole intersection type. */ + if (!ce || !instanceof_function(arg_ce, ce)) { + return false; + } + } ZEND_TYPE_LIST_FOREACH_END(); + return true; +} + static zend_always_inline bool zend_check_type_slow( zend_type *type, zval *arg, zend_reference *ref, void **cache_slot, bool is_return_type, bool is_internal) @@ -1024,11 +1055,19 @@ static zend_always_inline bool zend_check_type_slow( return true; } else { ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(*type), list_type) { - ce = zend_fetch_ce_from_cache_slot(cache_slot, list_type); - /* Instance of a single type part of a union is sufficient to pass the type check */ - if (ce && instanceof_function(Z_OBJCE_P(arg), ce)) { - return true; + if (ZEND_TYPE_IS_INTERSECTION(*list_type)) { + if (zend_check_intersection_type_from_cache_slot(ZEND_TYPE_LIST(*list_type), Z_OBJCE_P(arg), cache_slot)) { + return true; + } + } else { + ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*list_type)); + ce = zend_fetch_ce_from_cache_slot(cache_slot, list_type); + /* Instance of a single type part of a union is sufficient to pass the type check */ + if (ce && instanceof_function(Z_OBJCE_P(arg), ce)) { + return true; + } } + if (HAVE_CACHE_SLOT) { cache_slot++; } @@ -1048,9 +1087,6 @@ static zend_always_inline bool zend_check_type_slow( if ((type_mask & MAY_BE_CALLABLE) && zend_is_callable(arg, 0, NULL)) { return 1; } - if ((type_mask & MAY_BE_ITERABLE) && zend_is_iterable(arg)) { - return 1; - } if ((type_mask & MAY_BE_STATIC) && zend_value_instanceof_static(arg)) { return 1; } @@ -2929,7 +2965,7 @@ static zend_always_inline bool check_type_array_assignable(zend_type type) { if (!ZEND_TYPE_IS_SET(type)) { return 1; } - return (ZEND_TYPE_FULL_MASK(type) & (MAY_BE_ITERABLE|MAY_BE_ARRAY)) != 0; + return (ZEND_TYPE_FULL_MASK(type) & MAY_BE_ARRAY) != 0; } /* Checks whether an array can be assigned to the reference. Throws error if not assignable. */ @@ -3378,9 +3414,6 @@ static zend_always_inline int i_zend_verify_type_assignable_zval( type_mask = ZEND_TYPE_FULL_MASK(type); ZEND_ASSERT(!(type_mask & (MAY_BE_CALLABLE|MAY_BE_STATIC))); - if (type_mask & MAY_BE_ITERABLE) { - return zend_is_iterable(zv); - } /* SSTH Exception: IS_LONG may be accepted as IS_DOUBLE (converted) */ if (strict) { diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 3921ee35fe0d6..389e23290f81b 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -328,21 +328,6 @@ static bool unlinked_instanceof(zend_class_entry *ce1, zend_class_entry *ce2) { return 0; } -static bool zend_type_contains_traversable(zend_type type) { - zend_type *single_type; - if (ZEND_TYPE_FULL_MASK(type) & MAY_BE_OBJECT) { - return 1; - } - - ZEND_TYPE_FOREACH(type, single_type) { - if (ZEND_TYPE_HAS_NAME(*single_type) - && zend_string_equals_literal_ci(ZEND_TYPE_NAME(*single_type), "Traversable")) { - return 1; - } - } ZEND_TYPE_FOREACH_END(); - return 0; -} - static bool zend_type_permits_self( zend_type type, zend_class_entry *scope, zend_class_entry *self) { if (ZEND_TYPE_FULL_MASK(type) & MAY_BE_OBJECT) { @@ -473,15 +458,6 @@ static inheritance_status zend_is_class_subtype_of_type( return INHERITANCE_SUCCESS; } } - if (ZEND_TYPE_FULL_MASK(proto_type) & MAY_BE_ITERABLE) { - if (!fe_ce) fe_ce = lookup_class(fe_scope, fe_class_name); - if (!fe_ce) { - have_unresolved = 1; - } else if (unlinked_instanceof(fe_ce, zend_ce_traversable)) { - track_class_dependency(fe_ce, fe_class_name); - return INHERITANCE_SUCCESS; - } - } zend_type *single_type; @@ -489,6 +465,28 @@ static inheritance_status zend_is_class_subtype_of_type( * class is the subtype of at least one of them (union) or all of them (intersection). */ bool is_intersection = ZEND_TYPE_IS_INTERSECTION(proto_type); ZEND_TYPE_FOREACH(proto_type, single_type) { + if (ZEND_TYPE_IS_INTERSECTION(*single_type)) { + inheritance_status subtype_status = zend_is_class_subtype_of_type( + fe_scope, fe_class_name, proto_scope, *single_type); + + switch (subtype_status) { + case INHERITANCE_ERROR: + if (is_intersection) { + return INHERITANCE_ERROR; + } + continue; + case INHERITANCE_UNRESOLVED: + have_unresolved = 1; + continue; + case INHERITANCE_SUCCESS: + if (!is_intersection) { + return INHERITANCE_SUCCESS; + } + continue; + EMPTY_SWITCH_DEFAULT_CASE(); + } + } + zend_class_entry *proto_ce; zend_string *proto_class_name = NULL; if (ZEND_TYPE_HAS_NAME(*single_type)) { @@ -549,6 +547,72 @@ static void register_unresolved_classes(zend_class_entry *scope, zend_type type) } ZEND_TYPE_FOREACH_END(); } +static inheritance_status zend_is_intersection_subtype_of_type( + zend_class_entry *fe_scope, zend_type fe_type, + zend_class_entry *proto_scope, zend_type proto_type) +{ + bool have_unresolved = false; + zend_type *single_type; + uint32_t proto_type_mask = ZEND_TYPE_PURE_MASK(proto_type); + + /* Currently, for object type any class name would be allowed here. + * We still perform a class lookup for forward-compatibility reasons, + * as we may have named types in the future that are not classes + * (such as typedefs). */ + if (proto_type_mask & MAY_BE_OBJECT) { + ZEND_TYPE_FOREACH(fe_type, single_type) { + zend_string *fe_class_name = get_class_from_type(fe_scope, *single_type); + if (!fe_class_name) { + continue; + } + zend_class_entry *fe_ce = lookup_class(fe_scope, fe_class_name); + if (fe_ce) { + track_class_dependency(fe_ce, fe_class_name); + return INHERITANCE_SUCCESS; + } else { + have_unresolved = true; + } + } ZEND_TYPE_FOREACH_END(); + } + + /* U_1&...&U_n < V_1&...&V_m if forall V_j. exists U_i. U_i < V_j. + * U_1&...&U_n < V_1|...|V_m if exists V_j. exists U_i. U_i < V_j. + * As such, we need to iterate over proto_type (V_j) first and use a different + * quantifier depending on whether fe_type is a union or an intersection. */ + inheritance_status early_exit_status = + ZEND_TYPE_IS_INTERSECTION(proto_type) ? INHERITANCE_ERROR : INHERITANCE_SUCCESS; + ZEND_TYPE_FOREACH(proto_type, single_type) { + inheritance_status status; + + if (ZEND_TYPE_IS_INTERSECTION(*single_type)) { + status = zend_is_intersection_subtype_of_type( + fe_scope, fe_type, proto_scope, *single_type); + } else { + zend_string *proto_class_name = get_class_from_type(proto_scope, *single_type); + if (!proto_class_name) { + continue; + } + + zend_class_entry *proto_ce = NULL; + status = zend_is_intersection_subtype_of_class( + fe_scope, fe_type, proto_scope, proto_class_name, proto_ce); + } + + if (status == early_exit_status) { + return status; + } + if (status == INHERITANCE_UNRESOLVED) { + have_unresolved = true; + } + } ZEND_TYPE_FOREACH_END(); + + if (have_unresolved) { + return INHERITANCE_UNRESOLVED; + } + + return early_exit_status == INHERITANCE_ERROR ? INHERITANCE_SUCCESS : INHERITANCE_ERROR; +} + static inheritance_status zend_perform_covariant_type_check( zend_class_entry *fe_scope, zend_type fe_type, zend_class_entry *proto_scope, zend_type proto_type) @@ -567,18 +631,6 @@ static inheritance_status zend_perform_covariant_type_check( uint32_t proto_type_mask = ZEND_TYPE_PURE_MASK(proto_type); uint32_t added_types = fe_type_mask & ~proto_type_mask; if (added_types) { - // TODO: Make "iterable" an alias of "array|Traversable" instead, - // so these special cases will be handled automatically. - if ((added_types & MAY_BE_ITERABLE) - && (proto_type_mask & MAY_BE_ARRAY) - && zend_type_contains_traversable(proto_type)) { - /* Replacing array|Traversable with iterable is okay */ - added_types &= ~MAY_BE_ITERABLE; - } - if ((added_types & MAY_BE_ARRAY) && (proto_type_mask & MAY_BE_ITERABLE)) { - /* Replacing iterable with array is okay */ - added_types &= ~MAY_BE_ARRAY; - } if ((added_types & MAY_BE_STATIC) && zend_type_permits_self(proto_type, proto_scope, fe_scope)) { /* Replacing type that accepts self with static is okay */ @@ -601,51 +653,17 @@ static inheritance_status zend_perform_covariant_type_check( bool have_unresolved = false; if (ZEND_TYPE_IS_INTERSECTION(fe_type)) { - /* Currently, for object type any class name would be allowed here. - * We still perform a class lookup for forward-compatibility reasons, - * as we may have named types in the future that are not classes - * (such as typedefs). */ - if (proto_type_mask & (MAY_BE_OBJECT|MAY_BE_ITERABLE)) { - bool any_class = (proto_type_mask & MAY_BE_OBJECT) != 0; - ZEND_TYPE_FOREACH(fe_type, single_type) { - zend_string *fe_class_name = get_class_from_type(fe_scope, *single_type); - if (!fe_class_name) { - continue; - } - zend_class_entry *fe_ce = lookup_class(fe_scope, fe_class_name); - if (fe_ce) { - if (any_class || unlinked_instanceof(fe_ce, zend_ce_traversable)) { - track_class_dependency(fe_ce, fe_class_name); - return INHERITANCE_SUCCESS; - } - } else { - have_unresolved = true; - } - } ZEND_TYPE_FOREACH_END(); - } - - /* U_1&...&U_n < V_1&...&V_m if forall V_j. exists U_i. U_i < V_j. - * U_1&...&U_n < V_1|...|V_m if exists V_j. exists U_i. U_i < V_j. - * As such, we need to iterate over proto_type (V_j) first and use a different - * quantifier depending on whether fe_type is a union or an intersection. */ early_exit_status = ZEND_TYPE_IS_INTERSECTION(proto_type) ? INHERITANCE_ERROR : INHERITANCE_SUCCESS; - ZEND_TYPE_FOREACH(proto_type, single_type) { - zend_string *proto_class_name = get_class_from_type(proto_scope, *single_type); - if (!proto_class_name) { - continue; - } + inheritance_status status = zend_is_intersection_subtype_of_type( + fe_scope, fe_type, proto_scope, proto_type); - zend_class_entry *proto_ce = NULL; - inheritance_status status = zend_is_intersection_subtype_of_class( - fe_scope, fe_type, proto_scope, proto_class_name, proto_ce); - if (status == early_exit_status) { - return status; - } - if (status == INHERITANCE_UNRESOLVED) { - have_unresolved = true; - } - } ZEND_TYPE_FOREACH_END(); + if (status == early_exit_status) { + return status; + } + if (status == INHERITANCE_UNRESOLVED) { + have_unresolved = true; + } } else { /* U_1|...|U_n < V_1|...|V_m if forall U_i. exists V_j. U_i < V_j. * U_1|...|U_n < V_1&...&V_m if forall U_i. forall V_j. U_i < V_j. @@ -653,13 +671,21 @@ static inheritance_status zend_perform_covariant_type_check( * whether proto_type is a union or intersection (only the inner check differs). */ early_exit_status = INHERITANCE_ERROR; ZEND_TYPE_FOREACH(fe_type, single_type) { - zend_string *fe_class_name = get_class_from_type(fe_scope, *single_type); - if (!fe_class_name) { - continue; + inheritance_status status; + /* Union has an intersection type as it's member */ + if (ZEND_TYPE_IS_INTERSECTION(*single_type)) { + status = zend_is_intersection_subtype_of_type( + fe_scope, *single_type, proto_scope, proto_type); + } else { + zend_string *fe_class_name = get_class_from_type(fe_scope, *single_type); + if (!fe_class_name) { + continue; + } + + status = zend_is_class_subtype_of_type( + fe_scope, fe_class_name, proto_scope, proto_type); } - inheritance_status status = zend_is_class_subtype_of_type( - fe_scope, fe_class_name, proto_scope, proto_type); if (status == early_exit_status) { return status; } diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index a333bf52a8dad..284df029f1582 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -814,8 +814,18 @@ type: ; union_type: - type '|' type { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $1, $3); } - | union_type '|' type { $$ = zend_ast_list_add($1, $3); } + type '|' type + { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $1, $3); } + | '(' intersection_type ')' '|' type + { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $2, $5); } + | type '|' '(' intersection_type ')' + { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $1, $4); } + | '(' intersection_type ')' '|' '(' intersection_type ')' + { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $2, $6); } + | union_type '|' type + { $$ = zend_ast_list_add($1, $3); } + | union_type '|' '(' intersection_type ')' + { $$ = zend_ast_list_add($1, $4); } ; intersection_type: @@ -842,8 +852,16 @@ type_without_static: union_type_without_static: type_without_static '|' type_without_static { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $1, $3); } + | '(' intersection_type_without_static ')' '|' type_without_static + { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $2, $5); } + | type_without_static '|' '(' intersection_type_without_static ')' + { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $1, $4); } + | '(' intersection_type_without_static ')' '|' '(' intersection_type_without_static ')' + { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $2, $6); } | union_type_without_static '|' type_without_static { $$ = zend_ast_list_add($1, $3); } + | union_type_without_static '|' '(' intersection_type_without_static ')' + { $$ = zend_ast_list_add($1, $4); } ; intersection_type_without_static: diff --git a/Zend/zend_string.h b/Zend/zend_string.h index 1a1c0cd7a67bf..7c346077a3adc 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -580,6 +580,7 @@ EMPTY_SWITCH_DEFAULT_CASE() _(ZEND_STR_FALSE, "false") \ _(ZEND_STR_NULL_LOWERCASE, "null") \ _(ZEND_STR_MIXED, "mixed") \ + _(ZEND_STR_TRAVERSABLE, "Traversable") \ _(ZEND_STR_SLEEP, "__sleep") \ _(ZEND_STR_WAKEUP, "__wakeup") \ _(ZEND_STR_CASES, "cases") \ diff --git a/Zend/zend_type_info.h b/Zend/zend_type_info.h index 93048a777bdb3..f9780181a4c67 100644 --- a/Zend/zend_type_info.h +++ b/Zend/zend_type_info.h @@ -38,7 +38,6 @@ /* These are used in zend_type, but not for type inference. * They are allowed to overlap with types used during inference. */ #define MAY_BE_CALLABLE (1 << IS_CALLABLE) -#define MAY_BE_ITERABLE (1 << IS_ITERABLE) #define MAY_BE_VOID (1 << IS_VOID) #define MAY_BE_NEVER (1 << IS_NEVER) #define MAY_BE_STATIC (1 << IS_STATIC) diff --git a/Zend/zend_types.h b/Zend/zend_types.h index 151afadec2258..908a2c769a909 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -144,7 +144,8 @@ typedef struct { #define _ZEND_TYPE_NAME_BIT (1u << 24) #define _ZEND_TYPE_LIST_BIT (1u << 22) #define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT) -/* TODO: bit 21 is not used */ +/* For BC behaviour with iterable type */ +#define _ZEND_TYPE_ITERABLE_BIT (1u << 21) /* Whether the type list is arena allocated */ #define _ZEND_TYPE_ARENA_BIT (1u << 20) /* Whether the type list is an intersection type */ @@ -170,6 +171,9 @@ typedef struct { #define ZEND_TYPE_HAS_LIST(t) \ ((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0) +#define ZEND_TYPE_IS_ITERABLE_FALLBACK(t) \ + ((((t).type_mask) & _ZEND_TYPE_ITERABLE_BIT) != 0) + #define ZEND_TYPE_IS_INTERSECTION(t) \ ((((t).type_mask) & _ZEND_TYPE_INTERSECTION_BIT) != 0) @@ -263,7 +267,7 @@ typedef struct { { NULL, (_type_mask) } #define ZEND_TYPE_INIT_CODE(code, allow_null, extra_flags) \ - ZEND_TYPE_INIT_MASK(((code) == _IS_BOOL ? MAY_BE_BOOL : ((code) == IS_MIXED ? MAY_BE_ANY : (1 << (code)))) \ + ZEND_TYPE_INIT_MASK(((code) == _IS_BOOL ? MAY_BE_BOOL : ( (code) == IS_ITERABLE ? _ZEND_TYPE_ITERABLE_BIT : ((code) == IS_MIXED ? MAY_BE_ANY : (1 << (code))))) \ | ((allow_null) ? _ZEND_TYPE_NULLABLE_BIT : 0) | (extra_flags)) #define ZEND_TYPE_INIT_PTR(ptr, type_kind, allow_null, extra_flags) \ diff --git a/build/gen_stub.php b/build/gen_stub.php index 320fe087ab895..647426a2c65bf 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -219,7 +219,6 @@ public static function fromString(string $typeString): SimpleType case "float": case "string": case "callable": - case "iterable": case "object": case "resource": case "mixed": @@ -231,6 +230,8 @@ public static function fromString(string $typeString): SimpleType return ArrayType::createGenericArray(); case "self": throw new Exception('The exact class name must be used instead of "self"'); + case "iterable": + throw new Exception('This should not happen'); } $matches = []; @@ -369,8 +370,6 @@ public function toTypeCode(): string { return "IS_VOID"; case "callable": return "IS_CALLABLE"; - case "iterable": - return "IS_ITERABLE"; case "mixed": return "IS_MIXED"; case "static": @@ -408,8 +407,6 @@ public function toTypeMask(): string { return "MAY_BE_OBJECT"; case "callable": return "MAY_BE_CALLABLE"; - case "iterable": - return "MAY_BE_ITERABLE"; case "mixed": return "MAY_BE_ANY"; case "void": @@ -509,18 +506,32 @@ class Type { public static function fromNode(Node $node): Type { if ($node instanceof Node\UnionType) { - return new Type(array_map(['SimpleType', 'fromNode'], $node->types)); + $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\NullableType) { return new Type( [ - SimpleType::fromNode($node->type), + ...Type::fromNode($node->type)->types, SimpleType::null(), ] ); } + if ($node instanceof Node\Identifier && $node->toLowerString() === "iterable") { + return new Type( + [ + SimpleType::fromString("Traversable"), + ArrayType::createGenericArray(), + ] + ); + } + return new Type([SimpleType::fromNode($node)]); } diff --git a/ext/opcache/jit/zend_jit_arm64.dasc b/ext/opcache/jit/zend_jit_arm64.dasc index ec50f6edd8deb..112f007f3d192 100644 --- a/ext/opcache/jit/zend_jit_arm64.dasc +++ b/ext/opcache/jit/zend_jit_arm64.dasc @@ -12232,7 +12232,7 @@ static int zend_jit_fetch_obj(dasm_State **Dst, uint32_t flags = opline->extended_value & ZEND_FETCH_OBJ_FLAGS; if (flags == ZEND_FETCH_DIM_WRITE) { - if ((ZEND_TYPE_FULL_MASK(prop_info->type) & (MAY_BE_ITERABLE|MAY_BE_ARRAY)) == 0) { + if ((ZEND_TYPE_FULL_MASK(prop_info->type) & MAY_BE_ARRAY) == 0) { if (!type_loaded) { type_loaded = 1; | MEM_ACCESS_32_WITH_UOFFSET ldr, REG2w, FCARG1x, (prop_info->offset + offsetof(zval,u1.type_info)), TMP1 diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c index 9ec73892d5475..955d8fc1cfc53 100644 --- a/ext/opcache/jit/zend_jit_helpers.c +++ b/ext/opcache/jit/zend_jit_helpers.c @@ -1949,7 +1949,7 @@ static zend_always_inline bool check_type_array_assignable(zend_type type) { if (!ZEND_TYPE_IS_SET(type)) { return 1; } - return (ZEND_TYPE_FULL_MASK(type) & (MAY_BE_ITERABLE|MAY_BE_ARRAY)) != 0; + return (ZEND_TYPE_FULL_MASK(type) & MAY_BE_ARRAY) != 0; } static zend_property_info *zend_object_fetch_property_type_info( @@ -2094,7 +2094,7 @@ static void ZEND_FASTCALL zend_jit_check_array_promotion(zval *val, zend_propert if ((Z_TYPE_P(val) <= IS_FALSE || (Z_ISREF_P(val) && Z_TYPE_P(Z_REFVAL_P(val)) <= IS_FALSE)) && ZEND_TYPE_IS_SET(prop->type) - && (ZEND_TYPE_FULL_MASK(prop->type) & (MAY_BE_ITERABLE|MAY_BE_ARRAY)) == 0) { + && (ZEND_TYPE_FULL_MASK(prop->type) & MAY_BE_ARRAY) == 0) { zend_string *type_str = zend_type_to_string(prop->type); zend_type_error( "Cannot auto-initialize an array inside property %s::$%s of type %s", diff --git a/ext/opcache/jit/zend_jit_x86.dasc b/ext/opcache/jit/zend_jit_x86.dasc index c4a715b306e57..c33a4912d7b51 100644 --- a/ext/opcache/jit/zend_jit_x86.dasc +++ b/ext/opcache/jit/zend_jit_x86.dasc @@ -12962,7 +12962,7 @@ static int zend_jit_fetch_obj(dasm_State **Dst, uint32_t flags = opline->extended_value & ZEND_FETCH_OBJ_FLAGS; if (flags == ZEND_FETCH_DIM_WRITE) { - if ((ZEND_TYPE_FULL_MASK(prop_info->type) & (MAY_BE_ITERABLE|MAY_BE_ARRAY)) == 0) { + if ((ZEND_TYPE_FULL_MASK(prop_info->type) & MAY_BE_ARRAY) == 0) { if (!type_loaded) { type_loaded = 1; | mov edx, dword [FCARG1a + prop_info->offset + 8] diff --git a/ext/opcache/tests/iterable_type_optimization.phpt b/ext/opcache/tests/iterable_type_optimization.phpt index 277df8f374c26..340fcdea22811 100644 --- a/ext/opcache/tests/iterable_type_optimization.phpt +++ b/ext/opcache/tests/iterable_type_optimization.phpt @@ -12,7 +12,7 @@ test(new stdClass); ?> --EXPECTF-- -Fatal error: Uncaught TypeError: test(): Return value must be of type iterable, stdClass returned in %s:%d +Fatal error: Uncaught TypeError: test(): Return value must be of type Traversable|array, stdClass returned in %s:%d Stack trace: #0 %s(%d): test(Object(stdClass)) #1 {main} diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 8c7118bda6d89..5c9bcc731b475 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -1330,6 +1330,7 @@ typedef enum { } reflection_type_kind; /* For backwards compatibility reasons, we need to return T|null style unions + * and transformation from iterable to Traversable|array * as a ReflectionNamedType. Here we determine what counts as a union type and * what doesn't. */ static reflection_type_kind get_type_kind(zend_type type) { @@ -1344,6 +1345,10 @@ static reflection_type_kind get_type_kind(zend_type type) { } if (ZEND_TYPE_IS_COMPLEX(type)) { + /* BC support for 'iterable' type */ + if (UNEXPECTED(ZEND_TYPE_IS_ITERABLE_FALLBACK(type))) { + return NAMED_TYPE; + } if (type_mask_without_null != 0) { return UNION_TYPE; } @@ -2726,6 +2731,11 @@ ZEND_METHOD(ReflectionParameter, isArray) } GET_REFLECTION_OBJECT_PTR(param); + /* BC For iterable */ + if (ZEND_TYPE_IS_ITERABLE_FALLBACK(param->arg_info->type)) { + RETURN_FALSE; + } + type_mask = ZEND_TYPE_PURE_MASK_WITHOUT_NULL(param->arg_info->type); RETVAL_BOOL(type_mask == MAY_BE_ARRAY); } @@ -3006,9 +3016,21 @@ ZEND_METHOD(ReflectionType, allowsNull) } /* }}} */ +/* For BC with iterable for named types */ +static zend_string *zend_named_reflection_type_to_string(zend_type type) { + if (ZEND_TYPE_IS_ITERABLE_FALLBACK(type)) { + zend_string *iterable = ZSTR_KNOWN(ZEND_STR_ITERABLE); + if (ZEND_TYPE_FULL_MASK(type) & MAY_BE_NULL) { + return zend_string_concat2("?", strlen("?"), ZSTR_VAL(iterable), ZSTR_LEN(iterable)); + } + return iterable; + } + return zend_type_to_string(type); +} + static zend_string *zend_type_to_string_without_null(zend_type type) { ZEND_TYPE_FULL_MASK(type) &= ~MAY_BE_NULL; - return zend_type_to_string(type); + return zend_named_reflection_type_to_string(type); } /* {{{ Return the text of the type hint */ @@ -3022,7 +3044,7 @@ ZEND_METHOD(ReflectionType, __toString) } GET_REFLECTION_OBJECT_PTR(param); - RETURN_STR(zend_type_to_string(param->type)); + RETURN_STR(zend_named_reflection_type_to_string(param->type)); } /* }}} */ @@ -3040,7 +3062,7 @@ ZEND_METHOD(ReflectionNamedType, getName) if (param->legacy_behavior) { RETURN_STR(zend_type_to_string_without_null(param->type)); } - RETURN_STR(zend_type_to_string(param->type)); + RETURN_STR(zend_named_reflection_type_to_string(param->type)); } /* }}} */ @@ -3055,6 +3077,10 @@ ZEND_METHOD(ReflectionNamedType, isBuiltin) } GET_REFLECTION_OBJECT_PTR(param); + if (ZEND_TYPE_IS_ITERABLE_FALLBACK(param->type)) { + RETURN_TRUE; + } + /* Treat "static" as a class type for the purposes of reflection. */ RETVAL_BOOL(ZEND_TYPE_IS_ONLY_MASK(param->type) && !(ZEND_TYPE_FULL_MASK(param->type) & MAY_BE_STATIC)); @@ -3063,6 +3089,11 @@ ZEND_METHOD(ReflectionNamedType, isBuiltin) static void append_type(zval *return_value, zend_type type) { zval reflection_type; + /* Drop iterable BC bit for type list */ + if (ZEND_TYPE_IS_ITERABLE_FALLBACK(type)) { + ZEND_TYPE_FULL_MASK(type) &= ~_ZEND_TYPE_ITERABLE_BIT; + } + reflection_type_factory(type, &reflection_type, 0); zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &reflection_type); } @@ -3103,9 +3134,6 @@ ZEND_METHOD(ReflectionUnionType, getTypes) if (type_mask & MAY_BE_CALLABLE) { append_type_mask(return_value, MAY_BE_CALLABLE); } - if (type_mask & MAY_BE_ITERABLE) { - append_type_mask(return_value, MAY_BE_ITERABLE); - } if (type_mask & MAY_BE_OBJECT) { append_type_mask(return_value, MAY_BE_OBJECT); } diff --git a/ext/reflection/tests/bug72661.phpt b/ext/reflection/tests/bug72661.phpt deleted file mode 100644 index b1cb764beb62a..0000000000000 --- a/ext/reflection/tests/bug72661.phpt +++ /dev/null @@ -1,10 +0,0 @@ ---TEST-- -Bug #72661 (ReflectionType::__toString crashes with iterable) ---FILE-- -getType()); -?> ---EXPECT-- -string(8) "iterable" diff --git a/ext/reflection/tests/iterable_Reflection.phpt b/ext/reflection/tests/iterable_Reflection.phpt new file mode 100644 index 0000000000000..0a8a30a6dd0c4 --- /dev/null +++ b/ext/reflection/tests/iterable_Reflection.phpt @@ -0,0 +1,98 @@ +--TEST-- +iterable Type in Reflection +--FILE-- +getType(); +var_dump($paramType::class); +var_dump($paramType); +var_dump($paramType->getName()); +var_dump((string) $paramType); +var_dump($paramType->isBuiltin()); + +$reflectionFunc = new ReflectionFunction($function); +$returnType = $reflectionFunc->getReturnType(); +var_dump($returnType::class); +var_dump($returnType); +var_dump($returnType->getName()); +var_dump((string) $returnType); +var_dump($returnType->isBuiltin()); + +class PropIterableTypeTest { + public iterable $iterable; + public ?iterable $nullableIterable; + public array $control; + public ?array $nullableControl; +} + +$reflector = new ReflectionClass(PropIterableTypeTest::class); + +[$property, $nullable, $control, $nullableControl] = $reflector->getProperties(); +$iterableType = $property->getType(); +var_dump($iterableType::class); +var_dump($iterableType); +var_dump($iterableType->getName()); +var_dump((string) $iterableType); +var_dump($iterableType->isBuiltin()); + +$nullableIterableType = $nullable->getType(); +var_dump($nullableIterableType::class); +var_dump($nullableIterableType); +var_dump($nullableIterableType->getName()); +var_dump((string) $nullableIterableType); +var_dump($nullableIterableType->isBuiltin()); + +$controlType = $control->getType(); +var_dump($controlType::class); +var_dump($controlType); +var_dump($controlType->getName()); +var_dump((string) $controlType); +var_dump($controlType->isBuiltin()); + +$nullableControlType = $nullableControl->getType(); +var_dump($nullableControlType::class); +var_dump($nullableControlType); +var_dump($nullableControlType->getName()); +var_dump((string) $nullableControlType); +var_dump($nullableControlType->isBuiltin()); + +?> +--EXPECTF-- +string(19) "ReflectionNamedType" +object(ReflectionNamedType)#%d (0) { +} +string(8) "iterable" +string(8) "iterable" +bool(true) +string(19) "ReflectionNamedType" +object(ReflectionNamedType)#%d (0) { +} +string(8) "iterable" +string(8) "iterable" +bool(true) +string(19) "ReflectionNamedType" +object(ReflectionNamedType)#%d (0) { +} +string(8) "iterable" +string(8) "iterable" +bool(true) +string(19) "ReflectionNamedType" +object(ReflectionNamedType)#%d (0) { +} +string(8) "iterable" +string(9) "?iterable" +bool(true) +string(19) "ReflectionNamedType" +object(ReflectionNamedType)#%d (0) { +} +string(5) "array" +string(5) "array" +bool(true) +string(19) "ReflectionNamedType" +object(ReflectionNamedType)#%d (0) { +} +string(5) "array" +string(6) "?array" +bool(true) diff --git a/ext/reflection/tests/ReflectionType_001.phpt b/ext/reflection/tests/types/ReflectionType_001.phpt similarity index 98% rename from ext/reflection/tests/ReflectionType_001.phpt rename to ext/reflection/tests/types/ReflectionType_001.phpt index d0f327d0466ff..b4baf5355c038 100644 --- a/ext/reflection/tests/ReflectionType_001.phpt +++ b/ext/reflection/tests/types/ReflectionType_001.phpt @@ -80,7 +80,6 @@ class PropTypeTest { public int $int; public string $string; public array $arr; - public iterable $iterable; public stdClass $std; public OtherThing $other; public $mixed; @@ -216,7 +215,6 @@ string(4) "Test" public int $int; public string $string; public array $arr; -public iterable $iterable; public stdClass $std; public OtherThing $other; public $mixed; diff --git a/ext/reflection/tests/ReflectionType_002.phpt b/ext/reflection/tests/types/ReflectionType_002.phpt similarity index 100% rename from ext/reflection/tests/ReflectionType_002.phpt rename to ext/reflection/tests/types/ReflectionType_002.phpt diff --git a/ext/reflection/tests/ReflectionType_possible_types.phpt b/ext/reflection/tests/types/ReflectionType_possible_types.phpt similarity index 93% rename from ext/reflection/tests/ReflectionType_possible_types.phpt rename to ext/reflection/tests/types/ReflectionType_possible_types.phpt index dd6d39300b590..9162b71df98df 100644 --- a/ext/reflection/tests/ReflectionType_possible_types.phpt +++ b/ext/reflection/tests/types/ReflectionType_possible_types.phpt @@ -11,7 +11,6 @@ $functions = [ function(): bool {}, function(): array {}, function(): callable {}, - function(): iterable {}, function(): null {}, function(): false {}, function(): StdClass {} @@ -31,7 +30,6 @@ string(6) "string" string(4) "bool" string(5) "array" string(8) "callable" -string(8) "iterable" string(4) "null" string(5) "false" string(8) "StdClass" diff --git a/ext/reflection/tests/types/dnf_types.phpt b/ext/reflection/tests/types/dnf_types.phpt new file mode 100644 index 0000000000000..46f3ab0f55f14 --- /dev/null +++ b/ext/reflection/tests/types/dnf_types.phpt @@ -0,0 +1,85 @@ +--TEST-- +Disjunctive Normal Form types in reflection +--FILE-- +getTypes() as $type) { + if ($type instanceof ReflectionNamedType) { + echo $str_indent . " Name: " . $type->getName() . "\n"; + echo $str_indent . " String: " . (string) $type . "\n"; + echo $str_indent . " Allows Null: " . json_encode($type->allowsNull()) . "\n"; + } else { + dumpType($type, $indent+1); + } + } +} + +function test1(): (X&Y)|(Z&Traversable)|Countable { } + +class Test { + public (X&Y)|Countable $prop; +} + +dumpType((new ReflectionFunction('test1'))->getReturnType()); + +$rc = new ReflectionClass(Test::class); +$rp = $rc->getProperty('prop'); +dumpType($rp->getType()); + +/* Force CE resolution of the property type */ + +interface y {} +class x implements Y, Countable { + public function count(): int { return 0; } +} +$test = new Test; +$test->prop = new x; + +$rp = $rc->getProperty('prop'); +dumpType($rp->getType()); + +?> +--EXPECT-- +Type (X&Y)|(Z&Traversable)|Countable is ReflectionUnionType: + Type X&Y is ReflectionIntersectionType: + Name: X + String: X + Allows Null: false + Name: Y + String: Y + Allows Null: false + Type Z&Traversable is ReflectionIntersectionType: + Name: Z + String: Z + Allows Null: false + Name: Traversable + String: Traversable + Allows Null: false + Name: Countable + String: Countable + Allows Null: false +Type (X&Y)|Countable is ReflectionUnionType: + Type X&Y is ReflectionIntersectionType: + Name: X + String: X + Allows Null: false + Name: Y + String: Y + Allows Null: false + Name: Countable + String: Countable + Allows Null: false +Type (X&Y)|Countable is ReflectionUnionType: + Type X&Y is ReflectionIntersectionType: + Name: X + String: X + Allows Null: false + Name: Y + String: Y + Allows Null: false + Name: Countable + String: Countable + Allows Null: false diff --git a/ext/reflection/tests/types/dnf_types_with_null.phpt b/ext/reflection/tests/types/dnf_types_with_null.phpt new file mode 100644 index 0000000000000..ade2199399587 --- /dev/null +++ b/ext/reflection/tests/types/dnf_types_with_null.phpt @@ -0,0 +1,81 @@ +--TEST-- +Disjunctive Normal Form types in reflection +--FILE-- +allowsNull()) . "\n"; + foreach ($rt->getTypes() as $type) { + if ($type instanceof ReflectionNamedType) { + echo $str_indent . " Name: " . $type->getName() . "\n"; + echo $str_indent . " String: " . (string) $type . "\n"; + } else { + dumpType($type, $indent+1); + } + } +} + +function test1(): (X&Y)|(Z&Traversable)|null { } + +class Test { + public (X&Y)|null $prop; +} + +dumpType((new ReflectionFunction('test1'))->getReturnType()); + +$rc = new ReflectionClass(Test::class); +$rp = $rc->getProperty('prop'); +dumpType($rp->getType()); + +/* Force CE resolution of the property type */ + +interface y {} +class x implements Y, Countable { + public function count(): int { return 0; } +} +$test = new Test; +$test->prop = new x; + +$rp = $rc->getProperty('prop'); +dumpType($rp->getType()); + +?> +--EXPECT-- +Type (X&Y)|(Z&Traversable)|null is ReflectionUnionType: +Allows Null: true + Type X&Y is ReflectionIntersectionType: + Allows Null: false + Name: X + String: X + Name: Y + String: Y + Type Z&Traversable is ReflectionIntersectionType: + Allows Null: false + Name: Z + String: Z + Name: Traversable + String: Traversable + Name: null + String: null +Type (X&Y)|null is ReflectionUnionType: +Allows Null: true + Type X&Y is ReflectionIntersectionType: + Allows Null: false + Name: X + String: X + Name: Y + String: Y + Name: null + String: null +Type (X&Y)|null is ReflectionUnionType: +Allows Null: true + Type X&Y is ReflectionIntersectionType: + Allows Null: false + Name: X + String: X + Name: Y + String: Y + Name: null + String: null diff --git a/ext/reflection/tests/intersection_types.phpt b/ext/reflection/tests/types/intersection_types.phpt similarity index 100% rename from ext/reflection/tests/intersection_types.phpt rename to ext/reflection/tests/types/intersection_types.phpt diff --git a/ext/reflection/tests/mixed_type.phpt b/ext/reflection/tests/types/mixed_type.phpt similarity index 100% rename from ext/reflection/tests/mixed_type.phpt rename to ext/reflection/tests/types/mixed_type.phpt diff --git a/ext/reflection/tests/union_types.phpt b/ext/reflection/tests/types/union_types.phpt similarity index 95% rename from ext/reflection/tests/union_types.phpt rename to ext/reflection/tests/types/union_types.phpt index a3ac53b54ab29..e670567712f3b 100644 --- a/ext/reflection/tests/union_types.phpt +++ b/ext/reflection/tests/types/union_types.phpt @@ -75,13 +75,16 @@ Allows null: true Name: null String: null Allows Null: true -Type X|iterable|bool: +Type X|Traversable|array|bool: Allows null: false Name: X String: X Allows Null: false - Name: iterable - String: iterable + Name: Traversable + String: Traversable + Allows Null: false + Name: array + String: array Allows Null: false Name: bool String: bool diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 37bbc4547f8af..2fbabe7ef647b 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -314,6 +314,29 @@ static ZEND_FUNCTION(zend_iterable) ZEND_PARSE_PARAMETERS_END(); } +static ZEND_FUNCTION(zend_iterable_legacy) +{ + zval *arg1, *arg2; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ITERABLE(arg1) + Z_PARAM_OPTIONAL + Z_PARAM_ITERABLE_OR_NULL(arg2) + ZEND_PARSE_PARAMETERS_END(); + + RETURN_COPY(arg1); +} + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_iterable_legacy, 0, 1, IS_ITERABLE, 0) + ZEND_ARG_TYPE_INFO(0, arg1, IS_ITERABLE, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, arg2, IS_ITERABLE, 1, "null") +ZEND_END_ARG_INFO() + +static const zend_function_entry ext_function_legacy[] = { + ZEND_FE(zend_iterable_legacy, arginfo_zend_iterable_legacy) + ZEND_FE_END +}; + /* Call a method on a class or object using zend_call_method() */ static ZEND_FUNCTION(zend_call_method) { @@ -628,6 +651,8 @@ PHP_MINIT_FUNCTION(zend_test) zend_test_string_enum = register_class_ZendTestStringEnum(); zend_test_int_enum = register_class_ZendTestIntEnum(); + zend_register_functions(NULL, ext_function_legacy, NULL, EG(current_module)->type); + // Loading via dl() not supported with the observer API if (type != MODULE_TEMPORARY) { REGISTER_INI_ENTRIES(); diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index 0c575cf002ae0..d682b53755096 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -53,8 +53,8 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_zend_string_or_stdclass_or_n ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_iterable, 0, 1, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, arg1, IS_ITERABLE, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, arg2, IS_ITERABLE, 1, "null") + ZEND_ARG_OBJ_TYPE_MASK(0, arg1, Traversable, MAY_BE_ARRAY, NULL) + ZEND_ARG_OBJ_TYPE_MASK(0, arg2, Traversable, MAY_BE_ARRAY|MAY_BE_NULL, "null") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_weakmap_attach, 0, 2, _IS_BOOL, 0) diff --git a/ext/zend_test/tests/zend_legacy_iterable.phpt b/ext/zend_test/tests/zend_legacy_iterable.phpt new file mode 100644 index 0000000000000..73bbb269c2679 --- /dev/null +++ b/ext/zend_test/tests/zend_legacy_iterable.phpt @@ -0,0 +1,28 @@ +--TEST-- +Test that legacy IS_ITERABLE arg info type generates a notice +--EXTENSIONS-- +zend_test +--FILE-- + +==DONE== +--EXPECT-- +array(0) { +} +array(0) { +} +object(Generator)#1 (0) { +} +object(Generator)#1 (0) { +} +==DONE== diff --git a/ext/zend_test/tests/zend_weakmap.phpt b/ext/zend_test/tests/zend_weakmap.phpt index 99a2075c0471f..209b8bc0e0906 100644 --- a/ext/zend_test/tests/zend_weakmap.phpt +++ b/ext/zend_test/tests/zend_weakmap.phpt @@ -51,4 +51,4 @@ array(1) { [%s]=> object(stdClass)#2 (0) { } -} \ No newline at end of file +}