Skip to content

Make intersection types nullable #7259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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--
Expand All @@ -55,3 +61,6 @@ object(TestChild)#%d (0) {
}
object(TestParent)#%d (0) {
}
object(TestChild)#%d (0) {
}
NULL
4 changes: 2 additions & 2 deletions Zend/tests/type_declarations/intersection_types/bug81268.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php

Expand All @@ -9,4 +9,4 @@ class Test {

?>
--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
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
--TEST--
Intersection types cannot be implicitly nullable
Intersection types can be implicitly nullable as the others
--FILE--
<?php

function foo(X&Y $foo = null) {}
function foo(X&Y $foo = null) { return $foo; }

var_dump(foo());

?>
--EXPECTF--
Fatal error: Cannot use null as default value for parameter $foo of type X&Y in %s on line %d
--EXPECT--
NULL

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Intersection types and typed reference
--FILE--
<?php

interface X {}
interface Y {}
interface Z {}

class A implements X, Y, Z {}
class B implements X, Y {}

class Test {
public X&Y|null $y;
public X&Z $z;
}
$test = new Test;
$r = new A;
$test->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
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
Intersection types and typed reference
--FILE--
<?php

interface X {}
interface Y {}
interface Z {}

class A implements X, Y, Z {}
class B implements X, Y {}

class Test {
public X&Y|null $y;
public X&Z|null $z;
}
$test = new Test;
$r = new A;
$test->y =& $r;
$test->z =& $r;

$r = null;

?>
==DONE==
--EXPECT--
==DONE==
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
Invalid nullable widening
--FILE--
<?php

interface A {}
interface B {}
interface C {}

class Test implements A, B, C {}

class Foo {
public function foo(): A {
return new Test();
}
}

class FooChild extends Foo {
public function foo(): A&B {
return new Test();
}
}

class FooSecondChild extends FooChild {
public function foo(): A&B|null {
return new Test();
}
}

?>
--EXPECTF--
Fatal error: Declaration of FooSecondChild::foo(): A&B|null must be compatible with FooChild::foo(): A&B in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
Intersection type removing nullable
--FILE--
<?php

class A {}
class B extends A {}

class Test {
public A&B|null $prop;
}
class Test2 extends Test {
public A&B $prop;
}

?>
--EXPECTF--
Fatal error: Type of Test2::$prop must be A&B|null (as in class Test) in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
Invalid nullable narrowing
--FILE--
<?php

interface A {}
interface B {}

class Foo {
public function foo(A&B|null $foo) {
}
}

class FooChild extends Foo {
public function foo(A&B $foo) {
}
}

?>
--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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--TEST--
Valid intersection type variance
--FILE--
<?php

interface X {}
interface Y {}
interface Z {}

class TestParent implements X, Y, Z {}
class TestChild implements Z {}

class A {
public X&Y|null $prop;

public function method1(X&Y&Z $a): X&Y|null{}
public function method2(X&Y $a): ?X {}
}
class B extends A {
public X&Y|null $prop;

public function method1(X&Y|null $a): X&Y&Z {}
public function method2(?X $a): X&Y {}
}

?>
===DONE===
--EXPECT--
===DONE===
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
Valid inheritence - co-variance
--FILE--
<?php

interface A {}
interface B {}
interface C {}

class Test implements A, B, C {}

class Foo {
public function foo(): A&B|null {
return null;
}
}

class FooChild extends Foo {
public function foo(): A&B {
return new Test();
}
}

$o = new Foo();
var_dump($o->foo());
$o = new FooChild();
var_dump($o->foo());

?>
--EXPECTF--
NULL
object(Test)#%d (0) {
}
3 changes: 3 additions & 0 deletions Zend/zend_ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
13 changes: 2 additions & 11 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down
14 changes: 14 additions & 0 deletions Zend/zend_language_parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);

%type <ident> reserved_non_modifiers semi_reserved

%type <void> null_type

%% /* Rules */

start:
Expand Down Expand Up @@ -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; }
;

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

Expand Down