diff --git a/Zend/tests/type_declarations/standalone_null.phpt b/Zend/tests/type_declarations/standalone_null.phpt new file mode 100644 index 0000000000000..d96a1f471e483 --- /dev/null +++ b/Zend/tests/type_declarations/standalone_null.phpt @@ -0,0 +1,14 @@ +--TEST-- +Null can be used as a standalone type +--FILE-- + +--EXPECT-- +NULL diff --git a/Zend/tests/type_declarations/typed_properties_110.phpt b/Zend/tests/type_declarations/typed_properties_110.phpt new file mode 100644 index 0000000000000..6a108b4529c66 --- /dev/null +++ b/Zend/tests/type_declarations/typed_properties_110.phpt @@ -0,0 +1,20 @@ +--TEST-- +Test typed properties allow null +--FILE-- +value = null; + +try { + $foo->value = 1; +} catch (\TypeError $e) { + echo $e->getMessage(); +} + +?> +--EXPECT-- +Cannot assign int to property Foo::$value of type null diff --git a/Zend/tests/type_declarations/typed_properties_111.phpt b/Zend/tests/type_declarations/typed_properties_111.phpt new file mode 100644 index 0000000000000..c23fb72e5d657 --- /dev/null +++ b/Zend/tests/type_declarations/typed_properties_111.phpt @@ -0,0 +1,20 @@ +--TEST-- +Test typed properties allow false +--FILE-- +value = false; + +try { + $foo->value = true; +} catch (\TypeError $e) { + echo $e->getMessage(); +} + +?> +--EXPECT-- +Cannot assign bool to property Foo::$value of type false diff --git a/Zend/tests/type_declarations/typed_return_null_false_without_value.phpt b/Zend/tests/type_declarations/typed_return_null_false_without_value.phpt new file mode 100644 index 0000000000000..ae785c9a126d3 --- /dev/null +++ b/Zend/tests/type_declarations/typed_return_null_false_without_value.phpt @@ -0,0 +1,14 @@ +--TEST-- +Typed null|false return without value generates compile-time error +--FILE-- + +--EXPECTF-- +Fatal error: A function with return type must return a value (did you mean "return null;" instead of "return;"?) in %s on line %d diff --git a/Zend/tests/type_declarations/typed_return_null_without_value.phpt b/Zend/tests/type_declarations/typed_return_null_without_value.phpt new file mode 100644 index 0000000000000..9152622a4a338 --- /dev/null +++ b/Zend/tests/type_declarations/typed_return_null_without_value.phpt @@ -0,0 +1,14 @@ +--TEST-- +Typed null return without value generates compile-time error +--FILE-- + +--EXPECTF-- +Fatal error: A function with return type must return a value (did you mean "return null;" instead of "return;"?) in %s on line %d diff --git a/Zend/tests/type_declarations/union_types/null_false_union.phpt b/Zend/tests/type_declarations/union_types/null_false_union.phpt new file mode 100644 index 0000000000000..1cf6b46066189 --- /dev/null +++ b/Zend/tests/type_declarations/union_types/null_false_union.phpt @@ -0,0 +1,12 @@ +--TEST-- +Null and false can be used in a union type +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/union_types/redundant_types/nullable_null.phpt b/Zend/tests/type_declarations/union_types/redundant_types/nullable_null.phpt index 0e0e915bdd223..9c35978576237 100644 --- a/Zend/tests/type_declarations/union_types/redundant_types/nullable_null.phpt +++ b/Zend/tests/type_declarations/union_types/redundant_types/nullable_null.phpt @@ -8,4 +8,4 @@ function test(): ?null { ?> --EXPECTF-- -Fatal error: Null cannot be used as a standalone type in %s on line %d +Fatal error: null cannot be marked as nullable in %s on line %d diff --git a/Zend/tests/type_declarations/union_types/standalone_false.phpt b/Zend/tests/type_declarations/union_types/standalone_false.phpt index 76d8c9b7a1ede..1a31e23d22988 100644 --- a/Zend/tests/type_declarations/union_types/standalone_false.phpt +++ b/Zend/tests/type_declarations/union_types/standalone_false.phpt @@ -1,10 +1,11 @@ --TEST-- -False cannot be used as a standalone type +False can be used as a standalone type --FILE-- ---EXPECTF-- -Fatal error: False cannot be used as a standalone type in %s on line %d +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/union_types/standalone_false_implicit_nullability.phpt b/Zend/tests/type_declarations/union_types/standalone_false_implicit_nullability.phpt new file mode 100644 index 0000000000000..3baac82f7e905 --- /dev/null +++ b/Zend/tests/type_declarations/union_types/standalone_false_implicit_nullability.phpt @@ -0,0 +1,11 @@ +--TEST-- +False can be used as a standalone type even with implicit nullability +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/union_types/standalone_null.phpt b/Zend/tests/type_declarations/union_types/standalone_null.phpt index d66f27c117a75..a18375d68cb4e 100644 --- a/Zend/tests/type_declarations/union_types/standalone_null.phpt +++ b/Zend/tests/type_declarations/union_types/standalone_null.phpt @@ -1,10 +1,11 @@ --TEST-- -Null cannot be used as a standalone type +Null can be used as a standalone type --FILE-- ---EXPECTF-- -Fatal error: Null cannot be used as a standalone type in %s on line %d +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/tests/type_declarations/union_types/standalone_nullable_false.phpt b/Zend/tests/type_declarations/union_types/standalone_nullable_false.phpt index aa1bf34db1eb9..b4a3f88451ae0 100644 --- a/Zend/tests/type_declarations/union_types/standalone_nullable_false.phpt +++ b/Zend/tests/type_declarations/union_types/standalone_nullable_false.phpt @@ -1,10 +1,11 @@ --TEST-- -Nullable false cannot be used as a standalone type +Nullable false can be used as a standalone type --FILE-- ---EXPECTF-- -Fatal error: False cannot be used as a standalone type in %s on line %d +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index cfe44d3f3766f..444dee5b0d7d4 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6186,11 +6186,11 @@ static bool zend_type_contains_traversable(zend_type type) { static zend_type zend_compile_typename( zend_ast *ast, bool force_allow_null) /* {{{ */ { - bool allow_null = force_allow_null; + bool is_marked_nullable = ast->attr & ZEND_TYPE_NULLABLE; zend_ast_attr orig_ast_attr = ast->attr; zend_type type = ZEND_TYPE_INIT_NONE(0); - if (ast->attr & ZEND_TYPE_NULLABLE) { - allow_null = 1; + + if (is_marked_nullable) { ast->attr &= ~ZEND_TYPE_NULLABLE; } @@ -6314,10 +6314,6 @@ static zend_type zend_compile_typename( type = zend_compile_single_typename(ast); } - if (allow_null) { - ZEND_TYPE_FULL_MASK(type) |= MAY_BE_NULL; - } - 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); @@ -6332,7 +6328,7 @@ static zend_type zend_compile_typename( ZSTR_VAL(type_str)); } - if (type_mask == MAY_BE_ANY && (orig_ast_attr & ZEND_TYPE_NULLABLE)) { + 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"); } @@ -6343,6 +6339,15 @@ static zend_type zend_compile_typename( ZSTR_VAL(type_str)); } + if ((type_mask & MAY_BE_NULL) && is_marked_nullable) { + zend_error_noreturn(E_COMPILE_ERROR, "null cannot be marked as nullable"); + } + + if (is_marked_nullable || force_allow_null) { + ZEND_TYPE_FULL_MASK(type) |= MAY_BE_NULL; + type_mask = ZEND_TYPE_PURE_MASK(type); + } + if ((type_mask & MAY_BE_VOID) && (ZEND_TYPE_IS_COMPLEX(type) || type_mask != MAY_BE_VOID)) { zend_error_noreturn(E_COMPILE_ERROR, "Void can only be used as a standalone type"); } @@ -6351,15 +6356,6 @@ static zend_type zend_compile_typename( zend_error_noreturn(E_COMPILE_ERROR, "never can only be used as a standalone type"); } - if ((type_mask & (MAY_BE_NULL|MAY_BE_FALSE)) - && !ZEND_TYPE_IS_COMPLEX(type) && !(type_mask & ~(MAY_BE_NULL|MAY_BE_FALSE))) { - if (type_mask == MAY_BE_NULL) { - zend_error_noreturn(E_COMPILE_ERROR, "Null cannot be used as a standalone type"); - } else { - zend_error_noreturn(E_COMPILE_ERROR, "False cannot be used as a standalone type"); - } - } - ast->attr = orig_ast_attr; return type; } diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 84ef8bdfbb450..4f30cc2fca700 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -1356,6 +1356,7 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be type_reference *reference; reflection_type_kind type_kind = get_type_kind(type); bool is_mixed = ZEND_TYPE_PURE_MASK(type) == MAY_BE_ANY; + bool is_only_null = (ZEND_TYPE_PURE_MASK(type) == MAY_BE_NULL && !ZEND_TYPE_IS_COMPLEX(type)); switch (type_kind) { case INTERSECTION_TYPE: @@ -1373,7 +1374,7 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be intern = Z_REFLECTION_P(object); reference = (type_reference*) emalloc(sizeof(type_reference)); reference->type = type; - reference->legacy_behavior = legacy_behavior && type_kind == NAMED_TYPE && !is_mixed; + reference->legacy_behavior = legacy_behavior && type_kind == NAMED_TYPE && !is_mixed && !is_only_null; intern->ptr = reference; intern->ref_type = REF_TYPE_TYPE; diff --git a/ext/reflection/tests/ReflectionType_possible_types.phpt b/ext/reflection/tests/ReflectionType_possible_types.phpt index ccb87254663d3..dd6d39300b590 100644 --- a/ext/reflection/tests/ReflectionType_possible_types.phpt +++ b/ext/reflection/tests/ReflectionType_possible_types.phpt @@ -12,6 +12,8 @@ $functions = [ function(): array {}, function(): callable {}, function(): iterable {}, + function(): null {}, + function(): false {}, function(): StdClass {} ]; @@ -30,4 +32,6 @@ 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/union_types.phpt b/ext/reflection/tests/union_types.phpt index 8dbbbdab854a3..a3ac53b54ab29 100644 --- a/ext/reflection/tests/union_types.phpt +++ b/ext/reflection/tests/union_types.phpt @@ -13,8 +13,17 @@ function dumpType(ReflectionUnionType $rt) { } } +function dumpBCType(ReflectionNamedType $rt) { + echo "Type $rt:\n"; + echo " Name: " . $rt->getName() . "\n"; + echo " String: " . (string) $rt . "\n"; + echo " Allows Null: " . ($rt->allowsNull() ? "true" : "false") . "\n"; +} + function test1(): X|Y|int|float|false|null { } function test2(): X|iterable|bool { } +function test3(): null|false { } +function test4(): ?false { } class Test { public X|Y|int $prop; @@ -22,6 +31,8 @@ class Test { dumpType((new ReflectionFunction('test1'))->getReturnType()); dumpType((new ReflectionFunction('test2'))->getReturnType()); +dumpBCType((new ReflectionFunction('test3'))->getReturnType()); +dumpBCType((new ReflectionFunction('test4'))->getReturnType()); $rc = new ReflectionClass(Test::class); $rp = $rc->getProperty('prop'); @@ -75,6 +86,14 @@ Allows null: false Name: bool String: bool Allows Null: false +Type ?false: + Name: false + String: ?false + Allows Null: true +Type ?false: + Name: false + String: ?false + Allows Null: true Type X|Y|int: Allows null: false Name: X