Allow `null and false` as standalone types (#7546)

RFC: https://wiki.php.net/rfc/null-standalone-type

Also a drive-by consistency fix for error messages.
This commit is contained in:
George Peter Banyard 2022-04-08 17:23:52 +01:00 committed by GitHub
parent 7bb2a9ff38
commit 6039c07a3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 156 additions and 28 deletions

View File

@ -0,0 +1,14 @@
--TEST--
Null can be used as a standalone type
--FILE--
<?php
function test(null $v): null {
return $v;
}
var_dump(test(null));
?>
--EXPECT--
NULL

View File

@ -0,0 +1,20 @@
--TEST--
Test typed properties allow null
--FILE--
<?php
class Foo {
public null $value;
}
$foo = new Foo();
$foo->value = null;
try {
$foo->value = 1;
} catch (\TypeError $e) {
echo $e->getMessage();
}
?>
--EXPECT--
Cannot assign int to property Foo::$value of type null

View File

@ -0,0 +1,20 @@
--TEST--
Test typed properties allow false
--FILE--
<?php
class Foo {
public false $value;
}
$foo = new Foo();
$foo->value = false;
try {
$foo->value = true;
} catch (\TypeError $e) {
echo $e->getMessage();
}
?>
--EXPECT--
Cannot assign bool to property Foo::$value of type false

View File

@ -0,0 +1,14 @@
--TEST--
Typed null|false return without value generates compile-time error
--FILE--
<?php
function test() : null|false {
return;
}
test();
?>
--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

View File

@ -0,0 +1,14 @@
--TEST--
Typed null return without value generates compile-time error
--FILE--
<?php
function test() : null {
return;
}
test();
?>
--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

View File

@ -0,0 +1,12 @@
--TEST--
Null and false can be used in a union type
--FILE--
<?php
function test1(): null|false {}
function test2(): false|null {}
?>
===DONE===
--EXPECT--
===DONE===

View File

@ -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

View File

@ -1,10 +1,11 @@
--TEST--
False cannot be used as a standalone type
False can be used as a standalone type
--FILE--
<?php
function test(): false {}
?>
--EXPECTF--
Fatal error: False cannot be used as a standalone type in %s on line %d
===DONE===
--EXPECT--
===DONE===

View File

@ -0,0 +1,11 @@
--TEST--
False can be used as a standalone type even with implicit nullability
--FILE--
<?php
function test(false $v = null) {}
?>
===DONE===
--EXPECT--
===DONE===

View File

@ -1,10 +1,11 @@
--TEST--
Null cannot be used as a standalone type
Null can be used as a standalone type
--FILE--
<?php
function test(): null {}
?>
--EXPECTF--
Fatal error: Null cannot be used as a standalone type in %s on line %d
===DONE===
--EXPECT--
===DONE===

View File

@ -1,10 +1,11 @@
--TEST--
Nullable false cannot be used as a standalone type
Nullable false can be used as a standalone type
--FILE--
<?php
function test(): ?false {}
?>
--EXPECTF--
Fatal error: False cannot be used as a standalone type in %s on line %d
===DONE===
--EXPECT--
===DONE===

View File

@ -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;
}

View File

@ -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;

View File

@ -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"

View File

@ -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