Skip to content

Commit 1b0cdf8

Browse files
committed
Allow 'null' as a standalone type
1 parent 7d8e3da commit 1b0cdf8

14 files changed

+125
-24
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
===DONE===
14+
--EXPECT--
15+
NULL
16+
===DONE===
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
?>
11+
===DONE===
12+
--EXPECT--
13+
===DONE===
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

Zend/tests/type_declarations/union_types/standalone_false.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ function test(): false {}
77

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

Zend/tests/type_declarations/union_types/standalone_nullable_false.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ function test(): ?false {}
77

88
?>
99
--EXPECTF--
10-
Fatal error: False cannot be used as a standalone type in %s on line %d
10+
Fatal error: false cannot be marked as nullable since false is not a standalone type in %s on line %d

Zend/zend_compile.c

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,7 +1257,7 @@ zend_string *zend_type_to_string_resolved(zend_type type, zend_class_entry *scop
12571257

12581258
if (type_mask & MAY_BE_NULL) {
12591259
bool is_union = !str || memchr(ZSTR_VAL(str), '|', ZSTR_LEN(str)) != NULL;
1260-
if (!is_union) {
1260+
if (!is_union && !zend_string_equals_literal(str, "false")) {
12611261
zend_string *nullable_str = zend_string_concat2("?", 1, ZSTR_VAL(str), ZSTR_LEN(str));
12621262
zend_string_release(str);
12631263
return nullable_str;
@@ -6188,11 +6188,11 @@ static bool zend_type_contains_traversable(zend_type type) {
61886188
static zend_type zend_compile_typename(
61896189
zend_ast *ast, bool force_allow_null) /* {{{ */
61906190
{
6191-
bool allow_null = force_allow_null;
6191+
bool is_marked_nullable = false;
61926192
zend_ast_attr orig_ast_attr = ast->attr;
61936193
zend_type type = ZEND_TYPE_INIT_NONE(0);
61946194
if (ast->attr & ZEND_TYPE_NULLABLE) {
6195-
allow_null = 1;
6195+
is_marked_nullable = true;
61966196
ast->attr &= ~ZEND_TYPE_NULLABLE;
61976197
}
61986198

@@ -6316,10 +6316,6 @@ static zend_type zend_compile_typename(
63166316
type = zend_compile_single_typename(ast);
63176317
}
63186318

6319-
if (allow_null) {
6320-
ZEND_TYPE_FULL_MASK(type) |= MAY_BE_NULL;
6321-
}
6322-
63236319
uint32_t type_mask = ZEND_TYPE_PURE_MASK(type);
63246320
if ((type_mask & (MAY_BE_ARRAY|MAY_BE_ITERABLE)) == (MAY_BE_ARRAY|MAY_BE_ITERABLE)) {
63256321
zend_string *type_str = zend_type_to_string(type);
@@ -6334,7 +6330,7 @@ static zend_type zend_compile_typename(
63346330
ZSTR_VAL(type_str));
63356331
}
63366332

6337-
if (type_mask == MAY_BE_ANY && (orig_ast_attr & ZEND_TYPE_NULLABLE)) {
6333+
if (type_mask == MAY_BE_ANY && is_marked_nullable) {
63386334
zend_error_noreturn(E_COMPILE_ERROR, "Type mixed cannot be marked as nullable since mixed already includes null");
63396335
}
63406336

@@ -6345,6 +6341,23 @@ static zend_type zend_compile_typename(
63456341
ZSTR_VAL(type_str));
63466342
}
63476343

6344+
if ((type_mask & MAY_BE_NULL) && is_marked_nullable) {
6345+
zend_error_noreturn(E_COMPILE_ERROR, "null cannot be marked as nullable");
6346+
}
6347+
6348+
if ((type_mask & MAY_BE_FALSE) && !ZEND_TYPE_IS_COMPLEX(type) && !(type_mask & ~MAY_BE_FALSE)) {
6349+
if (is_marked_nullable) {
6350+
zend_error_noreturn(E_COMPILE_ERROR, "false cannot be marked as nullable since false is not a standalone type");
6351+
} else {
6352+
zend_error_noreturn(E_COMPILE_ERROR, "false cannot be used as a standalone type");
6353+
}
6354+
}
6355+
6356+
if (is_marked_nullable || force_allow_null) {
6357+
ZEND_TYPE_FULL_MASK(type) |= MAY_BE_NULL;
6358+
type_mask = ZEND_TYPE_PURE_MASK(type);
6359+
}
6360+
63486361
if ((type_mask & MAY_BE_VOID) && (ZEND_TYPE_IS_COMPLEX(type) || type_mask != MAY_BE_VOID)) {
63496362
zend_error_noreturn(E_COMPILE_ERROR, "Void can only be used as a standalone type");
63506363
}
@@ -6353,15 +6366,6 @@ static zend_type zend_compile_typename(
63536366
zend_error_noreturn(E_COMPILE_ERROR, "never can only be used as a standalone type");
63546367
}
63556368

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

ext/reflection/php_reflection.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1342,6 +1342,10 @@ static reflection_type_kind get_type_kind(zend_type type) {
13421342
if (type_mask_without_null == MAY_BE_BOOL || ZEND_TYPE_PURE_MASK(type) == MAY_BE_ANY) {
13431343
return NAMED_TYPE;
13441344
}
1345+
/* null|false must be a union type */
1346+
if (ZEND_TYPE_PURE_MASK(type) == (MAY_BE_NULL|MAY_BE_FALSE)) {
1347+
return UNION_TYPE;
1348+
}
13451349
/* Check that only one bit is set. */
13461350
if ((type_mask_without_null & (type_mask_without_null - 1)) != 0) {
13471351
return UNION_TYPE;
@@ -1356,6 +1360,7 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be
13561360
type_reference *reference;
13571361
reflection_type_kind type_kind = get_type_kind(type);
13581362
bool is_mixed = ZEND_TYPE_PURE_MASK(type) == MAY_BE_ANY;
1363+
bool is_only_null = (ZEND_TYPE_PURE_MASK(type) == MAY_BE_NULL && !ZEND_TYPE_IS_COMPLEX(type));
13591364

13601365
switch (type_kind) {
13611366
case INTERSECTION_TYPE:
@@ -1373,7 +1378,7 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be
13731378
intern = Z_REFLECTION_P(object);
13741379
reference = (type_reference*) emalloc(sizeof(type_reference));
13751380
reference->type = type;
1376-
reference->legacy_behavior = legacy_behavior && type_kind == NAMED_TYPE && !is_mixed;
1381+
reference->legacy_behavior = legacy_behavior && type_kind == NAMED_TYPE && !is_mixed && !is_only_null;
13771382
intern->ptr = reference;
13781383
intern->ref_type = REF_TYPE_TYPE;
13791384

ext/reflection/tests/ReflectionType_possible_types.phpt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ $functions = [
1212
function(): array {},
1313
function(): callable {},
1414
function(): iterable {},
15+
function(): null {},
1516
function(): StdClass {}
1617
];
1718

@@ -30,4 +31,5 @@ string(4) "bool"
3031
string(5) "array"
3132
string(8) "callable"
3233
string(8) "iterable"
34+
string(4) "null"
3335
string(8) "StdClass"

ext/reflection/tests/union_types.phpt

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

1616
function test1(): X|Y|int|float|false|null { }
1717
function test2(): X|iterable|bool { }
18+
function test3(): null|false { }
1819

1920
class Test {
2021
public X|Y|int $prop;
2122
}
2223

2324
dumpType((new ReflectionFunction('test1'))->getReturnType());
2425
dumpType((new ReflectionFunction('test2'))->getReturnType());
26+
dumpType((new ReflectionFunction('test3'))->getReturnType());
2527

2628
$rc = new ReflectionClass(Test::class);
2729
$rp = $rc->getProperty('prop');
@@ -75,6 +77,14 @@ Allows null: false
7577
Name: bool
7678
String: bool
7779
Allows Null: false
80+
Type false|null:
81+
Allows null: true
82+
Name: false
83+
String: false
84+
Allows Null: false
85+
Name: null
86+
String: null
87+
Allows Null: true
7888
Type X|Y|int:
7989
Allows null: false
8090
Name: X

0 commit comments

Comments
 (0)