diff --git a/Zend/tests/type_declarations/intersection_types/assigning_intersection_types.phpt b/Zend/tests/type_declarations/intersection_types/assigning_intersection_types.phpt index 568e6e25afade..8a18d83b66656 100644 --- a/Zend/tests/type_declarations/intersection_types/assigning_intersection_types.phpt +++ b/Zend/tests/type_declarations/intersection_types/assigning_intersection_types.phpt @@ -20,6 +20,9 @@ class A { public function method2(X $a): X&Y { return new TestParent(); } + public function method3(X&Y|null $a): X&Y|null { + return $a; + } } $tp = new TestParent(); @@ -42,7 +45,10 @@ $r = $o->method1($tc); var_dump($r); $r = $o->method2($tc); var_dump($r); - +$r = $o->method3($tc); +var_dump($r); +$r = $o->method3(null); +var_dump($r); ?> --EXPECTF-- @@ -55,3 +61,6 @@ object(TestChild)#%d (0) { } object(TestParent)#%d (0) { } +object(TestChild)#%d (0) { +} +NULL diff --git a/Zend/tests/type_declarations/intersection_types/bug81268.phpt b/Zend/tests/type_declarations/intersection_types/bug81268.phpt index 19f86d4eeb960..6a317fea3dd17 100644 --- a/Zend/tests/type_declarations/intersection_types/bug81268.phpt +++ b/Zend/tests/type_declarations/intersection_types/bug81268.phpt @@ -1,5 +1,5 @@ --TEST-- -Bug #81268 Wrong message when using null as a default value for intersection types +Bug #81268 Message when using null as a default value for intersection types --FILE-- --EXPECTF-- -Fatal error: Cannot use null as default value for property Test::$y of type X&Y in %s on line %d +Fatal error: Default value for property of type X&Y may not be null. Use the nullable type X&Y|null to allow null default value in %s on line %d diff --git a/Zend/tests/type_declarations/intersection_types/implicit_nullable_intersection_type.phpt b/Zend/tests/type_declarations/intersection_types/implicit_nullable_intersection_type.phpt index fdac7576a78f8..23681a5cd5c69 100644 --- a/Zend/tests/type_declarations/intersection_types/implicit_nullable_intersection_type.phpt +++ b/Zend/tests/type_declarations/intersection_types/implicit_nullable_intersection_type.phpt @@ -1,10 +1,12 @@ --TEST-- -Intersection types cannot be implicitly nullable +Intersection types can be implicitly nullable as the others --FILE-- ---EXPECTF-- -Fatal error: Cannot use null as default value for parameter $foo of type X&Y in %s on line %d +--EXPECT-- +NULL diff --git a/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt b/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt deleted file mode 100644 index 1c35dfdf91c70..0000000000000 --- a/Zend/tests/type_declarations/intersection_types/invalid_types/invalid_nullable_type.phpt +++ /dev/null @@ -1,10 +0,0 @@ ---TEST-- -Intersection type cannot be nullable ---FILE-- - ---EXPECTF-- -Parse error: syntax error, unexpected token "&", expecting "{" in %s on line %d diff --git a/Zend/tests/type_declarations/intersection_types/parameter.phpt b/Zend/tests/type_declarations/intersection_types/parameter.phpt index d1c7de2243654..a84b2bef5ccb7 100644 --- a/Zend/tests/type_declarations/intersection_types/parameter.phpt +++ b/Zend/tests/type_declarations/intersection_types/parameter.phpt @@ -9,10 +9,11 @@ interface B {} class Foo implements A, B {} class Bar implements A {} -function foo(A&B $bar) { +function foo(A&B|null $bar) { var_dump($bar); } +foo(null); foo(new Foo()); try { @@ -23,6 +24,7 @@ try { ?> --EXPECTF-- +NULL object(Foo)#1 (0) { } -foo(): Argument #1 ($bar) must be of type A&B, Bar given, called in %s on line %d +foo(): Argument #1 ($bar) must be of type A&B|null, Bar given, called in %s on line %d diff --git a/Zend/tests/type_declarations/intersection_types/typed_reference2.phpt b/Zend/tests/type_declarations/intersection_types/typed_reference2.phpt new file mode 100644 index 0000000000000..ade1405c0e4fc --- /dev/null +++ b/Zend/tests/type_declarations/intersection_types/typed_reference2.phpt @@ -0,0 +1,31 @@ +--TEST-- +Intersection types and typed reference +--FILE-- +y =& $r; +$test->z =& $r; + + +try { + $r = null; +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +?> +--EXPECT-- +Cannot assign null to reference held by property Test::$z of type X&Z diff --git a/Zend/tests/type_declarations/intersection_types/typed_reference3.phpt b/Zend/tests/type_declarations/intersection_types/typed_reference3.phpt new file mode 100644 index 0000000000000..c5a78a86e6ac2 --- /dev/null +++ b/Zend/tests/type_declarations/intersection_types/typed_reference3.phpt @@ -0,0 +1,27 @@ +--TEST-- +Intersection types and typed reference +--FILE-- +y =& $r; +$test->z =& $r; + +$r = null; + +?> +==DONE== +--EXPECT-- +==DONE== diff --git a/Zend/tests/type_declarations/intersection_types/variance/invalid7.phpt b/Zend/tests/type_declarations/intersection_types/variance/invalid7.phpt new file mode 100644 index 0000000000000..8e102f7ff3c4f --- /dev/null +++ b/Zend/tests/type_declarations/intersection_types/variance/invalid7.phpt @@ -0,0 +1,32 @@ +--TEST-- +Invalid nullable widening +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of FooSecondChild::foo(): A&B|null must be compatible with FooChild::foo(): A&B in %s on line %d diff --git a/Zend/tests/type_declarations/intersection_types/variance/invalid8.phpt b/Zend/tests/type_declarations/intersection_types/variance/invalid8.phpt new file mode 100644 index 0000000000000..b05113e80d79c --- /dev/null +++ b/Zend/tests/type_declarations/intersection_types/variance/invalid8.phpt @@ -0,0 +1,18 @@ +--TEST-- +Intersection type removing nullable +--FILE-- + +--EXPECTF-- +Fatal error: Type of Test2::$prop must be A&B|null (as in class Test) in %s on line %d diff --git a/Zend/tests/type_declarations/intersection_types/variance/invalid9.phpt b/Zend/tests/type_declarations/intersection_types/variance/invalid9.phpt new file mode 100644 index 0000000000000..867b7fb6a0777 --- /dev/null +++ b/Zend/tests/type_declarations/intersection_types/variance/invalid9.phpt @@ -0,0 +1,21 @@ +--TEST-- +Invalid nullable narrowing +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of FooChild::foo(A&B $foo) must be compatible with Foo::foo(A&B|null $foo) in %s on line %d diff --git a/Zend/tests/type_declarations/intersection_types/variance/valid10.phpt b/Zend/tests/type_declarations/intersection_types/variance/valid10.phpt new file mode 100644 index 0000000000000..923ad8c20909e --- /dev/null +++ b/Zend/tests/type_declarations/intersection_types/variance/valid10.phpt @@ -0,0 +1,29 @@ +--TEST-- +Valid intersection type variance +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/intersection_types/variance/valid9.phpt b/Zend/tests/type_declarations/intersection_types/variance/valid9.phpt new file mode 100644 index 0000000000000..d0cf28775d79e --- /dev/null +++ b/Zend/tests/type_declarations/intersection_types/variance/valid9.phpt @@ -0,0 +1,33 @@ +--TEST-- +Valid inheritence - co-variance +--FILE-- +foo()); +$o = new FooChild(); +var_dump($o->foo()); + +?> +--EXPECTF-- +NULL +object(Test)#%d (0) { +} diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index a2bbf2c9c5dc3..ffc19e8c98aed 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1543,6 +1543,9 @@ static ZEND_COLD void zend_ast_export_type(smart_str *str, zend_ast *ast, int in } zend_ast_export_type(str, list->child[i], indent); } + if (ast->attr & ZEND_TYPE_NULLABLE) { + smart_str_appends(str, "|null"); + } return; } if (ast->attr & ZEND_TYPE_NULLABLE) { diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index f9f98b74b7d6f..894cf20caba20 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1284,7 +1284,7 @@ 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) { + if (!is_union && !ZEND_TYPE_IS_INTERSECTION(type)) { zend_string *nullable_str = zend_string_concat2("?", 1, ZSTR_VAL(str), ZSTR_LEN(str)); zend_string_release(str); return nullable_str; @@ -6694,15 +6694,6 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall zend_error_noreturn(E_COMPILE_ERROR, "never cannot be used as a parameter type"); } - if (force_nullable && ZEND_TYPE_IS_INTERSECTION(arg_info->type)) { - 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); - } - if (default_type != IS_UNDEF && default_type != IS_CONSTANT_AST && !force_nullable && !zend_is_valid_default_value(arg_info->type, &default_node.u.constant)) { zend_string *type_str = zend_type_to_string(arg_info->type); @@ -7333,7 +7324,7 @@ void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t flags, z if (ZEND_TYPE_IS_SET(type) && !Z_CONSTANT(value_zv) && !zend_is_valid_default_value(type, &value_zv)) { zend_string *str = zend_type_to_string(type); - if (Z_TYPE(value_zv) == IS_NULL && !ZEND_TYPE_IS_INTERSECTION(type)) { + if (Z_TYPE(value_zv) == IS_NULL) { ZEND_TYPE_FULL_MASK(type) |= MAY_BE_NULL; zend_string *nullable_str = zend_type_to_string(type); diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index ccf11bda8b8c3..c410755bc6099 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -287,6 +287,8 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type reserved_non_modifiers semi_reserved +%type null_type + %% /* Rules */ start: @@ -793,10 +795,21 @@ optional_type_without_static: | type_expr_without_static { $$ = $1; } ; +null_type: + T_STRING { + zend_string *str = Z_STR(((zend_ast_zval*)$1)->val); + if (!zend_string_equals_literal_ci(str, "null")) { + zend_error(E_PARSE, "Invalid compound type expression"); + } + zend_string_free(str); + } +; + type_expr: type { $$ = $1; } | '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } | union_type { $$ = $1; } + | intersection_type '|' null_type { $$ = $1; $$->attr |= ZEND_TYPE_NULLABLE; } | intersection_type { $$ = $1; } ; @@ -822,6 +835,7 @@ type_expr_without_static: type_without_static { $$ = $1; } | '?' type_without_static { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } | union_type_without_static { $$ = $1; } + | intersection_type_without_static '|' null_type { $$ = $1; $$->attr |= ZEND_TYPE_NULLABLE; } | intersection_type_without_static { $$ = $1; } ;