diff options
22 files changed, 262 insertions, 19 deletions
@@ -3,6 +3,7 @@ PHP NEWS ?? ??? 2016, PHP 7.1.0 - Core: + . Added nullable types. (Levi, Dmitry) . Added DFA optimization framework based on e-SSA form. (Dmitry, Nikita) . Added specialized opcode handlers (e.g. ZEND_ADD_LONG_NO_OVERFLOW). (Dmitry) diff --git a/Zend/tests/bug71428.1.phpt b/Zend/tests/bug71428.1.phpt index fbf342380f..e4d3a22f67 100644 --- a/Zend/tests/bug71428.1.phpt +++ b/Zend/tests/bug71428.1.phpt @@ -1,7 +1,5 @@ --TEST-- bug #71428.1: inheritance with null default values ---XFAIL-- -This is a BC break --FILE-- <?php class A { @@ -11,5 +9,5 @@ class B extends A { public function m(array $a = []) {} } --EXPECTF-- -Warning: Declaration of B::m(array $a = Array) should be compatible with A::m(array $a = NULL) in %sbug71428.1.php on line 7 +Warning: Declaration of B::m(array $a = Array) should be compatible with A::m(?array $a = NULL) in %sbug71428.1.php on line 7 diff --git a/Zend/tests/bug71428.3.phpt b/Zend/tests/bug71428.3.phpt index 65d397052e..558e87c56e 100644 --- a/Zend/tests/bug71428.3.phpt +++ b/Zend/tests/bug71428.3.phpt @@ -1,7 +1,5 @@ --TEST-- bug #71428: Validation type inheritance with = NULL ---XFAIL-- -This is a BC break --FILE-- <?php class A { } @@ -9,5 +7,5 @@ class B { public function m(A $a = NULL, $n) { echo "B.m";} }; class C extends B { public function m(A $a , $n) { echo "C.m";} }; ?> --EXPECTF-- -Warning: Declaration of C::m(A $a, $n) should be compatible with B::m(A $a = NULL, $n) in %sbug71428.3.php on line 4 +Warning: Declaration of C::m(A $a, $n) should be compatible with B::m(?A $a, $n) in %sbug71428.3.php on line 4 diff --git a/Zend/tests/bug72119.phpt b/Zend/tests/bug72119.phpt index b8f070a25a..064381ada0 100644 --- a/Zend/tests/bug72119.phpt +++ b/Zend/tests/bug72119.phpt @@ -14,5 +14,6 @@ class Hello implements Foo { } echo "OK\n"; ?> ---EXPECT-- -OK +--EXPECTF-- +Fatal error: Declaration of Hello::bar(array $baz = Array) must be compatible with Foo::bar(?array $baz = NULL) in %s on line %d + diff --git a/Zend/tests/nullable_types/array.phpt b/Zend/tests/nullable_types/array.phpt new file mode 100644 index 0000000000..7b0a2ed4d5 --- /dev/null +++ b/Zend/tests/nullable_types/array.phpt @@ -0,0 +1,17 @@ +--TEST-- +Explicitly nullable array type +--FILE-- +<?php + +function _array_(?array $v): ?array { + return $v; +} + +var_dump(_array_(null)); +var_dump(_array_([])); + +--EXPECT-- +NULL +array(0) { +} + diff --git a/Zend/tests/nullable_types/contravariant_nullable_param_succeeds.phpt b/Zend/tests/nullable_types/contravariant_nullable_param_succeeds.phpt new file mode 100644 index 0000000000..f4d1e315fc --- /dev/null +++ b/Zend/tests/nullable_types/contravariant_nullable_param_succeeds.phpt @@ -0,0 +1,19 @@ +--TEST-- +Subtype can add nullability to a parameter (contravariance) + +--FILE-- +<?php + +interface A { + function method(int $p); +} + +class B implements A { + function method(?int $p) { } +} + +$b = new B(); +$b->method(null); + +--EXPECT-- + diff --git a/Zend/tests/nullable_types/contravariant_nullable_return_fails.phpt b/Zend/tests/nullable_types/contravariant_nullable_return_fails.phpt new file mode 100644 index 0000000000..c9be479ead --- /dev/null +++ b/Zend/tests/nullable_types/contravariant_nullable_return_fails.phpt @@ -0,0 +1,17 @@ +--TEST-- +Return type cannot add nullability (contravariance) + +--FILE-- +<?php + +interface A { + function method(): int; +} + +interface B extends A { + function method(): ?int; +} + +--EXPECTF-- +Fatal error: Declaration of B::method(): ?int must be compatible with A::method(): int in %s on line %d + diff --git a/Zend/tests/nullable_types/covariant_nullable_param_fails.phpt b/Zend/tests/nullable_types/covariant_nullable_param_fails.phpt new file mode 100644 index 0000000000..65b2858e6b --- /dev/null +++ b/Zend/tests/nullable_types/covariant_nullable_param_fails.phpt @@ -0,0 +1,17 @@ +--TEST-- +Subtype cannot remove nullable parameter (covariance) + +--FILE-- +<?php + +interface A { + function method(?int $p); +} + +class B implements A { + function method(int $p) { } +} + +--EXPECTF-- +Fatal error: Declaration of B::method(int $p) must be compatible with A::method(?int $p) in %s on line %d + diff --git a/Zend/tests/nullable_types/covariant_nullable_return_succeds.phpt b/Zend/tests/nullable_types/covariant_nullable_return_succeds.phpt new file mode 100644 index 0000000000..5776f9b99d --- /dev/null +++ b/Zend/tests/nullable_types/covariant_nullable_return_succeds.phpt @@ -0,0 +1,16 @@ +--TEST-- +Nullable covariant return types + +--FILE-- +<?php + +interface A { + function method(): ?int; +} + +interface B extends A { + function method(): int; +} + +--EXPECT-- + diff --git a/Zend/tests/nullable_types/float.phpt b/Zend/tests/nullable_types/float.phpt new file mode 100644 index 0000000000..8e44524cf0 --- /dev/null +++ b/Zend/tests/nullable_types/float.phpt @@ -0,0 +1,16 @@ +--TEST-- +Explicitly nullable float type +--FILE-- +<?php + +function _float_(?float $v): ?float { + return $v; +} + +var_dump(_float_(null)); +var_dump(_float_(1.3)); + +--EXPECT-- +NULL +float(1.3) + diff --git a/Zend/tests/nullable_types/int.phpt b/Zend/tests/nullable_types/int.phpt new file mode 100644 index 0000000000..ec75132edb --- /dev/null +++ b/Zend/tests/nullable_types/int.phpt @@ -0,0 +1,16 @@ +--TEST-- +Explicitly nullable int type +--FILE-- +<?php + +function _int_(?int $v): ?int { + return $v; +} + +var_dump(_int_(null)); +var_dump(_int_(1)); + +--EXPECT-- +NULL +int(1) + diff --git a/Zend/tests/nullable_types/invariant_param_and_return_succeeds.phpt b/Zend/tests/nullable_types/invariant_param_and_return_succeeds.phpt new file mode 100644 index 0000000000..0542e52c0f --- /dev/null +++ b/Zend/tests/nullable_types/invariant_param_and_return_succeeds.phpt @@ -0,0 +1,24 @@ +--TEST-- +Invariant parameter and return types work with nullable types + +--FILE-- +<?php + +interface A { + function method(?int $i): ?int; +} + +class B implements A { + function method(?int $i): ?int { + return $i; + } +} + +$b = new B(); +var_dump($b->method(null)); +var_dump($b->method(1)); + +--EXPECT-- +NULL +int(1) + diff --git a/Zend/tests/nullable_types/nullable_type_parameters_do_not_have_default_value.phpt b/Zend/tests/nullable_types/nullable_type_parameters_do_not_have_default_value.phpt new file mode 100644 index 0000000000..13b25e0dac --- /dev/null +++ b/Zend/tests/nullable_types/nullable_type_parameters_do_not_have_default_value.phpt @@ -0,0 +1,17 @@ +--TEST-- +Explicit nullable types do not imply a default value + +--FILE-- +<?php + +function f(?callable $p) {} + +f(); + +--EXPECTF-- +Fatal error: Uncaught TypeError: Argument 1 passed to f() must be callable, none given, called in %s on line %d and defined in %s:%d +Stack trace: +#%d %s +#%d %s + thrown in %s on line %d + diff --git a/Zend/tests/nullable_types/string.phpt b/Zend/tests/nullable_types/string.phpt new file mode 100644 index 0000000000..ffc6591b6b --- /dev/null +++ b/Zend/tests/nullable_types/string.phpt @@ -0,0 +1,16 @@ +--TEST-- +Explicitly nullable string type +--FILE-- +<?php + +function _string_(?string $v): ?string { + return $v; +} + +var_dump(_string_(null)); +var_dump(_string_("php")); + +--EXPECT-- +NULL +string(3) "php" + diff --git a/Zend/tests/return_types/030.phpt b/Zend/tests/return_types/030.phpt new file mode 100644 index 0000000000..d1ceac8329 --- /dev/null +++ b/Zend/tests/return_types/030.phpt @@ -0,0 +1,23 @@ +--TEST-- +Nullable return value +--FILE-- +<?php +function foo($x) : ?array { + return $x; +} + +foo([]); +echo "ok\n"; +foo(null); +echo "ok\n"; +foo(0); +?> +--EXPECTF-- +ok +ok + +Fatal error: Uncaught TypeError: Return value of foo() must be of the type array, integer returned in %s030.php:3 +Stack trace: +#0 %s030.php(10): foo(0) +#1 {main} + thrown in %s030.php on line 3 diff --git a/Zend/tests/return_types/031.phpt b/Zend/tests/return_types/031.phpt new file mode 100644 index 0000000000..91ee2f8ce4 --- /dev/null +++ b/Zend/tests/return_types/031.phpt @@ -0,0 +1,14 @@ +--TEST-- +Nullable return type inheritance rules (non-nullable and nullable) +--FILE-- +<?php +class A { + function foo(): int {} +} +class B extends A { + function foo(): ?int {} +} +?> +DONE +--EXPECTF-- +Fatal error: Declaration of B::foo(): ?int must be compatible with A::foo(): int in %s031.php on line 7
\ No newline at end of file diff --git a/Zend/tests/return_types/032.phpt b/Zend/tests/return_types/032.phpt new file mode 100644 index 0000000000..00790b5d60 --- /dev/null +++ b/Zend/tests/return_types/032.phpt @@ -0,0 +1,14 @@ +--TEST-- +Nullable return type inheritance rules (nullable and non-nullable) +--FILE-- +<?php +class A { + function foo(): ?int {} +} +class B extends A { + function foo(): int {} +} +?> +DONE +--EXPECT-- +DONE diff --git a/Zend/tests/variadic/adding_additional_optional_parameter_error.phpt b/Zend/tests/variadic/adding_additional_optional_parameter_error.phpt index da96264609..4c6f36f10b 100644 --- a/Zend/tests/variadic/adding_additional_optional_parameter_error.phpt +++ b/Zend/tests/variadic/adding_additional_optional_parameter_error.phpt @@ -13,4 +13,4 @@ class MySQL implements DB { ?> --EXPECTF-- -Fatal error: Declaration of MySQL::query($query, int $extraParam = NULL, string ...$params) must be compatible with DB::query($query, string ...$params) in %s on line %d +Fatal error: Declaration of MySQL::query($query, ?int $extraParam = NULL, string ...$params) must be compatible with DB::query($query, string ...$params) in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index e401ce2430..0994681c20 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -4889,7 +4889,7 @@ static void zend_compile_typename(zend_ast *ast, zend_arg_info *arg_info) /* {{{ zend_uchar type = zend_lookup_builtin_type_by_name(class_name); if (type != 0) { - if (ast->attr != ZEND_NAME_NOT_FQ) { + if ((ast->attr & ZEND_NAME_NOT_FQ) != ZEND_NAME_NOT_FQ) { zend_error_noreturn(E_COMPILE_ERROR, "Scalar type declaration '%s' must be unqualified", ZSTR_VAL(zend_string_tolower(class_name))); @@ -4929,6 +4929,11 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast) /* {{{ */ arg_infos->allow_null = 0; arg_infos->class_name = NULL; + if (return_type_ast->attr & ZEND_TYPE_NULLABLE) { + arg_infos->allow_null = 1; + return_type_ast->attr &= ~ZEND_TYPE_NULLABLE; + } + zend_compile_typename(return_type_ast, arg_infos); arg_infos++; @@ -5017,9 +5022,10 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast) /* {{{ */ && (Z_TYPE(default_node.u.constant) == IS_NULL || (Z_TYPE(default_node.u.constant) == IS_CONSTANT && strcasecmp(Z_STRVAL(default_node.u.constant), "NULL") == 0)); + zend_bool is_explicitly_nullable = (type_ast->attr & ZEND_TYPE_NULLABLE) == ZEND_TYPE_NULLABLE; op_array->fn_flags |= ZEND_ACC_HAS_TYPE_HINTS; - arg_info->allow_null = has_null_default; + arg_info->allow_null = has_null_default || is_explicitly_nullable; zend_compile_typename(type_ast, arg_info); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 344293273a..0de475da11 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -839,6 +839,8 @@ ZEND_API void zend_assert_valid_class_name(const zend_string *const_name); #define ZEND_NAME_NOT_FQ 1 #define ZEND_NAME_RELATIVE 2 +#define ZEND_TYPE_NULLABLE (1<<8) + /* var status for backpatching */ #define BP_VAR_R 0 #define BP_VAR_W 1 diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index edb2bbe5af..44abfb6ffb 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -319,13 +319,11 @@ static zend_bool zend_do_perform_implementation_check(const zend_function *fe, c return 0; } -#if 0 // This introduces BC break described at https://bugs.php.net/bug.php?id=72119 if (proto_arg_info->type_hint && proto_arg_info->allow_null && !fe_arg_info->allow_null) { /* incompatible nullability */ return 0; } -#endif /* by-ref constraints on arguments are invariant */ if (fe_arg_info->pass_by_reference != proto_arg_info->pass_by_reference) { @@ -344,6 +342,10 @@ static zend_bool zend_do_perform_implementation_check(const zend_function *fe, c if (!zend_do_perform_type_hint_check(fe, fe->common.arg_info - 1, proto, proto->common.arg_info - 1)) { return 0; } + + if (fe->common.arg_info[-1].allow_null && !proto->common.arg_info[-1].allow_null) { + return 0; + } } return 1; } @@ -351,6 +353,11 @@ static zend_bool zend_do_perform_implementation_check(const zend_function *fe, c static ZEND_COLD void zend_append_type_hint(smart_str *str, const zend_function *fptr, zend_arg_info *arg_info, int return_hint) /* {{{ */ { + + if (arg_info->type_hint != IS_UNDEF && arg_info->allow_null) { + smart_str_appendc(str, '?'); + } + if (arg_info->class_name) { const char *class_name; size_t class_name_len; @@ -491,8 +498,6 @@ static ZEND_COLD zend_string *zend_get_function_declaration(const zend_function } else { smart_str_appends(&str, "NULL"); } - } else if (arg_info->type_hint && arg_info->allow_null) { - smart_str_appends(&str, " = NULL"); } if (++i < num_args) { @@ -590,7 +595,8 @@ static void do_inheritance_check_on_method(zend_function *child, zend_function * error_verb = "must"; } else if ((parent->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) && (!(child->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) || - !zend_do_perform_type_hint_check(child, child->common.arg_info - 1, parent, parent->common.arg_info - 1))) { + !zend_do_perform_type_hint_check(child, child->common.arg_info - 1, parent, parent->common.arg_info - 1) || + (child->common.arg_info[-1].allow_null && !parent->common.arg_info[-1].allow_null))) { error_level = E_COMPILE_ERROR; error_verb = "must"; } else { diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 5f233d6a3b..44c5cfb12b 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -251,7 +251,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type <ast> ctor_arguments alt_if_stmt_without_else trait_adaptation_list lexical_vars %type <ast> lexical_var_list encaps_list %type <ast> array_pair non_empty_array_pair_list array_pair_list -%type <ast> isset_variable type return_type +%type <ast> isset_variable type return_type type_expr %type <ast> identifier %type <num> returns_ref function is_reference is_variadic variable_modifiers @@ -644,7 +644,12 @@ parameter: optional_type: /* empty */ { $$ = NULL; } - | type { $$ = $1; } + | type_expr { $$ = $1; } +; + +type_expr: + type { $$ = $1; } + | '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } ; type: @@ -655,7 +660,7 @@ type: return_type: /* empty */ { $$ = NULL; } - | ':' type { $$ = $2; } + | ':' type_expr { $$ = $2; } ; argument_list: |