Skip to content

Commit 6039c07

Browse files
authored
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.
1 parent 7bb2a9f commit 6039c07

15 files changed

+156
-28
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
Null can be used as a standalone type
3+
--FILE--
4+
<?php
5+
6+
function test(null $v): null {
7+
return $v;
8+
}
9+
10+
var_dump(test(null));
11+
12+
?>
13+
--EXPECT--
14+
NULL
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
Test typed properties allow null
3+
--FILE--
4+
<?php
5+
class Foo {
6+
public null $value;
7+
}
8+
9+
$foo = new Foo();
10+
$foo->value = null;
11+
12+
try {
13+
$foo->value = 1;
14+
} catch (\TypeError $e) {
15+
echo $e->getMessage();
16+
}
17+
18+
?>
19+
--EXPECT--
20+
Cannot assign int to property Foo::$value of type null
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
Test typed properties allow false
3+
--FILE--
4+
<?php
5+
class Foo {
6+
public false $value;
7+
}
8+
9+
$foo = new Foo();
10+
$foo->value = false;
11+
12+
try {
13+
$foo->value = true;
14+
} catch (\TypeError $e) {
15+
echo $e->getMessage();
16+
}
17+
18+
?>
19+
--EXPECT--
20+
Cannot assign bool to property Foo::$value of type false
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
Typed null|false return without value generates compile-time error
3+
--FILE--
4+
<?php
5+
6+
function test() : null|false {
7+
return;
8+
}
9+
10+
test();
11+
12+
?>
13+
--EXPECTF--
14+
Fatal error: A function with return type must return a value (did you mean "return null;" instead of "return;"?) in %s on line %d
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
Typed null return without value generates compile-time error
3+
--FILE--
4+
<?php
5+
6+
function test() : null {
7+
return;
8+
}
9+
10+
test();
11+
12+
?>
13+
--EXPECTF--
14+
Fatal error: A function with return type must return a value (did you mean "return null;" instead of "return;"?) in %s on line %d
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Null and false can be used in a union type
3+
--FILE--
4+
<?php
5+
6+
function test1(): null|false {}
7+
function test2(): false|null {}
8+
9+
?>
10+
===DONE===
11+
--EXPECT--
12+
===DONE===

Zend/tests/type_declarations/union_types/redundant_types/nullable_null.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ function test(): ?null {
88

99
?>
1010
--EXPECTF--
11-
Fatal error: Null cannot be used as a standalone type in %s on line %d
11+
Fatal error: null cannot be marked as nullable in %s on line %d
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
--TEST--
2-
False cannot be used as a standalone type
2+
False can be used as a standalone type
33
--FILE--
44
<?php
55

66
function test(): false {}
77

88
?>
9-
--EXPECTF--
10-
Fatal error: False cannot be used as a standalone type in %s on line %d
9+
===DONE===
10+
--EXPECT--
11+
===DONE===
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
--TEST--
2+
False can be used as a standalone type even with implicit nullability
3+
--FILE--
4+
<?php
5+
6+
function test(false $v = null) {}
7+
8+
?>
9+
===DONE===
10+
--EXPECT--
11+
===DONE===
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
--TEST--
2-
Null cannot be used as a standalone type
2+
Null can be used as a standalone type
33
--FILE--
44
<?php
55

66
function test(): null {}
77

88
?>
9-
--EXPECTF--
10-
Fatal error: Null cannot be used as a standalone type in %s on line %d
9+
===DONE===
10+
--EXPECT--
11+
===DONE===
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
--TEST--
2-
Nullable false cannot be used as a standalone type
2+
Nullable false can be used as a standalone type
33
--FILE--
44
<?php
55

66
function test(): ?false {}
77

88
?>
9-
--EXPECTF--
10-
Fatal error: False cannot be used as a standalone type in %s on line %d
9+
===DONE===
10+
--EXPECT--
11+
===DONE===

Zend/zend_compile.c

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6186,11 +6186,11 @@ static bool zend_type_contains_traversable(zend_type type) {
61866186
static zend_type zend_compile_typename(
61876187
zend_ast *ast, bool force_allow_null) /* {{{ */
61886188
{
6189-
bool allow_null = force_allow_null;
6189+
bool is_marked_nullable = ast->attr & ZEND_TYPE_NULLABLE;
61906190
zend_ast_attr orig_ast_attr = ast->attr;
61916191
zend_type type = ZEND_TYPE_INIT_NONE(0);
6192-
if (ast->attr & ZEND_TYPE_NULLABLE) {
6193-
allow_null = 1;
6192+
6193+
if (is_marked_nullable) {
61946194
ast->attr &= ~ZEND_TYPE_NULLABLE;
61956195
}
61966196

@@ -6314,10 +6314,6 @@ static zend_type zend_compile_typename(
63146314
type = zend_compile_single_typename(ast);
63156315
}
63166316

6317-
if (allow_null) {
6318-
ZEND_TYPE_FULL_MASK(type) |= MAY_BE_NULL;
6319-
}
6320-
63216317
uint32_t type_mask = ZEND_TYPE_PURE_MASK(type);
63226318
if ((type_mask & (MAY_BE_ARRAY|MAY_BE_ITERABLE)) == (MAY_BE_ARRAY|MAY_BE_ITERABLE)) {
63236319
zend_string *type_str = zend_type_to_string(type);
@@ -6332,7 +6328,7 @@ static zend_type zend_compile_typename(
63326328
ZSTR_VAL(type_str));
63336329
}
63346330

6335-
if (type_mask == MAY_BE_ANY && (orig_ast_attr & ZEND_TYPE_NULLABLE)) {
6331+
if (type_mask == MAY_BE_ANY && is_marked_nullable) {
63366332
zend_error_noreturn(E_COMPILE_ERROR, "Type mixed cannot be marked as nullable since mixed already includes null");
63376333
}
63386334

@@ -6343,6 +6339,15 @@ static zend_type zend_compile_typename(
63436339
ZSTR_VAL(type_str));
63446340
}
63456341

6342+
if ((type_mask & MAY_BE_NULL) && is_marked_nullable) {
6343+
zend_error_noreturn(E_COMPILE_ERROR, "null cannot be marked as nullable");
6344+
}
6345+
6346+
if (is_marked_nullable || force_allow_null) {
6347+
ZEND_TYPE_FULL_MASK(type) |= MAY_BE_NULL;
6348+
type_mask = ZEND_TYPE_PURE_MASK(type);
6349+
}
6350+
63466351
if ((type_mask & MAY_BE_VOID) && (ZEND_TYPE_IS_COMPLEX(type) || type_mask != MAY_BE_VOID)) {
63476352
zend_error_noreturn(E_COMPILE_ERROR, "Void can only be used as a standalone type");
63486353
}
@@ -6351,15 +6356,6 @@ static zend_type zend_compile_typename(
63516356
zend_error_noreturn(E_COMPILE_ERROR, "never can only be used as a standalone type");
63526357
}
63536358

6354-
if ((type_mask & (MAY_BE_NULL|MAY_BE_FALSE))
6355-
&& !ZEND_TYPE_IS_COMPLEX(type) && !(type_mask & ~(MAY_BE_NULL|MAY_BE_FALSE))) {
6356-
if (type_mask == MAY_BE_NULL) {
6357-
zend_error_noreturn(E_COMPILE_ERROR, "Null cannot be used as a standalone type");
6358-
} else {
6359-
zend_error_noreturn(E_COMPILE_ERROR, "False cannot be used as a standalone type");
6360-
}
6361-
}
6362-
63636359
ast->attr = orig_ast_attr;
63646360
return type;
63656361
}

ext/reflection/php_reflection.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,7 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be
13561356
type_reference *reference;
13571357
reflection_type_kind type_kind = get_type_kind(type);
13581358
bool is_mixed = ZEND_TYPE_PURE_MASK(type) == MAY_BE_ANY;
1359+
bool is_only_null = (ZEND_TYPE_PURE_MASK(type) == MAY_BE_NULL && !ZEND_TYPE_IS_COMPLEX(type));
13591360

13601361
switch (type_kind) {
13611362
case INTERSECTION_TYPE:
@@ -1373,7 +1374,7 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be
13731374
intern = Z_REFLECTION_P(object);
13741375
reference = (type_reference*) emalloc(sizeof(type_reference));
13751376
reference->type = type;
1376-
reference->legacy_behavior = legacy_behavior && type_kind == NAMED_TYPE && !is_mixed;
1377+
reference->legacy_behavior = legacy_behavior && type_kind == NAMED_TYPE && !is_mixed && !is_only_null;
13771378
intern->ptr = reference;
13781379
intern->ref_type = REF_TYPE_TYPE;
13791380

ext/reflection/tests/ReflectionType_possible_types.phpt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ $functions = [
1212
function(): array {},
1313
function(): callable {},
1414
function(): iterable {},
15+
function(): null {},
16+
function(): false {},
1517
function(): StdClass {}
1618
];
1719

@@ -30,4 +32,6 @@ string(4) "bool"
3032
string(5) "array"
3133
string(8) "callable"
3234
string(8) "iterable"
35+
string(4) "null"
36+
string(5) "false"
3337
string(8) "StdClass"

ext/reflection/tests/union_types.phpt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,26 @@ function dumpType(ReflectionUnionType $rt) {
1313
}
1414
}
1515

16+
function dumpBCType(ReflectionNamedType $rt) {
17+
echo "Type $rt:\n";
18+
echo " Name: " . $rt->getName() . "\n";
19+
echo " String: " . (string) $rt . "\n";
20+
echo " Allows Null: " . ($rt->allowsNull() ? "true" : "false") . "\n";
21+
}
22+
1623
function test1(): X|Y|int|float|false|null { }
1724
function test2(): X|iterable|bool { }
25+
function test3(): null|false { }
26+
function test4(): ?false { }
1827

1928
class Test {
2029
public X|Y|int $prop;
2130
}
2231

2332
dumpType((new ReflectionFunction('test1'))->getReturnType());
2433
dumpType((new ReflectionFunction('test2'))->getReturnType());
34+
dumpBCType((new ReflectionFunction('test3'))->getReturnType());
35+
dumpBCType((new ReflectionFunction('test4'))->getReturnType());
2536

2637
$rc = new ReflectionClass(Test::class);
2738
$rp = $rc->getProperty('prop');
@@ -75,6 +86,14 @@ Allows null: false
7586
Name: bool
7687
String: bool
7788
Allows Null: false
89+
Type ?false:
90+
Name: false
91+
String: ?false
92+
Allows Null: true
93+
Type ?false:
94+
Name: false
95+
String: ?false
96+
Allows Null: true
7897
Type X|Y|int:
7998
Allows null: false
8099
Name: X

0 commit comments

Comments
 (0)