diff --git a/Zend/tests/never-parameters/allowed_on_abstract_method.phpt b/Zend/tests/never-parameters/allowed_on_abstract_method.phpt new file mode 100644 index 0000000000000..0a4481c28f4a0 --- /dev/null +++ b/Zend/tests/never-parameters/allowed_on_abstract_method.phpt @@ -0,0 +1,11 @@ +--TEST-- +`never` parameter types - allowed on abstract methods +--FILE-- + +--EXPECT-- diff --git a/Zend/tests/never-parameters/allowed_on_interfaces.phpt b/Zend/tests/never-parameters/allowed_on_interfaces.phpt new file mode 100644 index 0000000000000..62c72f397666f --- /dev/null +++ b/Zend/tests/never-parameters/allowed_on_interfaces.phpt @@ -0,0 +1,11 @@ +--TEST-- +`never` parameter types - allowed on interfaces' methods +--FILE-- + +--EXPECT-- diff --git a/Zend/tests/never-parameters/ast_output.phpt b/Zend/tests/never-parameters/ast_output.phpt new file mode 100644 index 0000000000000..2bb4274d783e3 --- /dev/null +++ b/Zend/tests/never-parameters/ast_output.phpt @@ -0,0 +1,19 @@ +--TEST-- +`never` parameter types - AST output from assertions +--FILE-- +getMessage() . "\n"; +} +?> +--EXPECT-- +assert(false && new class { + public function invalid(never $v) { + } + +}) diff --git a/Zend/tests/never-parameters/backed_enum.phpt b/Zend/tests/never-parameters/backed_enum.phpt new file mode 100644 index 0000000000000..99f3daf1659ce --- /dev/null +++ b/Zend/tests/never-parameters/backed_enum.phpt @@ -0,0 +1,91 @@ +--TEST-- +`never` parameter types - BackedEnum uses never, backed enums have parameter types +--FILE-- +hasPrototype()) { + $proto = $method->getPrototype(); + echo "Prototype: " . $proto->class . "::" . $proto->name . "\n"; + } + $param = $method->getParameters()[0]; + echo $param . "\n"; + + $type = $param->getType(); + + assert($type instanceof ReflectionNamedType); + echo "Name: " . $type->getName() . "\n"; + echo "isBuiltin: " . $type->isBuiltIn() . "\n"; + echo "allowsNull: " . (int)$type->allowsNull() . "\n"; +} + +enum MyBoolean: int { + case FALSE = 0; + case TRUE = 1; +} +enum CardSuit: string { + case HEARTS = 'Hearts'; + case DIAMONDS = 'Diamonds'; + case CLUBS = 'Clubs'; + case SPADES = 'Spades'; +} + +enumMethod(BackedEnum::class, "from"); +echo "\n"; +enumMethod(BackedEnum::class, "tryFrom"); +echo "\n\n"; + +enumMethod(MyBoolean::class, "from"); +echo "\n"; +enumMethod(MyBoolean::class, "tryFrom"); +echo "\n\n"; + +enumMethod(CardSuit::class, "from"); +echo "\n"; +enumMethod(CardSuit::class, "tryFrom"); + +?> +--EXPECT-- +BackedEnum::from(): +Parameter #0 [ never $value ] +Name: never +isBuiltin: 1 +allowsNull: 0 + +BackedEnum::tryFrom(): +Parameter #0 [ never $value ] +Name: never +isBuiltin: 1 +allowsNull: 0 + + +MyBoolean::from(): +Prototype: BackedEnum::from +Parameter #0 [ int $value ] +Name: int +isBuiltin: 1 +allowsNull: 0 + +MyBoolean::tryFrom(): +Prototype: BackedEnum::tryFrom +Parameter #0 [ int $value ] +Name: int +isBuiltin: 1 +allowsNull: 0 + + +CardSuit::from(): +Prototype: BackedEnum::from +Parameter #0 [ string $value ] +Name: string +isBuiltin: 1 +allowsNull: 0 + +CardSuit::tryFrom(): +Prototype: BackedEnum::tryFrom +Parameter #0 [ string $value ] +Name: string +isBuiltin: 1 +allowsNull: 0 diff --git a/Zend/tests/never-parameters/basic_usage_abstract.phpt b/Zend/tests/never-parameters/basic_usage_abstract.phpt new file mode 100644 index 0000000000000..ce86a36a170db --- /dev/null +++ b/Zend/tests/never-parameters/basic_usage_abstract.phpt @@ -0,0 +1,42 @@ +--TEST-- +`never` parameter types - basic usage with an abstract class +--FILE-- +value; + } + public function set(int $v): void { + $this->value = $v; + } +} + +$box = new BoxedInt(5); +var_dump($box); +var_dump($box->get()); +$box->set(7); +var_dump($box); +var_dump($box->get()); + +?> +--EXPECTF-- +object(BoxedInt)#%d (1) { + ["value":"BoxedInt":private]=> + int(5) +} +int(5) +object(BoxedInt)#%d (1) { + ["value":"BoxedInt":private]=> + int(7) +} +int(7) diff --git a/Zend/tests/never-parameters/basic_usage_interface.phpt b/Zend/tests/never-parameters/basic_usage_interface.phpt new file mode 100644 index 0000000000000..c65c9e8e39a5a --- /dev/null +++ b/Zend/tests/never-parameters/basic_usage_interface.phpt @@ -0,0 +1,42 @@ +--TEST-- +`never` parameter types - basic usage with an interface +--FILE-- +value; + } + public function set(int $v): void { + $this->value = $v; + } +} + +$box = new BoxedInt(5); +var_dump($box); +var_dump($box->get()); +$box->set(7); +var_dump($box); +var_dump($box->get()); + +?> +--EXPECTF-- +object(BoxedInt)#%d (1) { + ["value":"BoxedInt":private]=> + int(5) +} +int(5) +object(BoxedInt)#%d (1) { + ["value":"BoxedInt":private]=> + int(7) +} +int(7) diff --git a/Zend/tests/never-parameters/cannot_narrow.phpt b/Zend/tests/never-parameters/cannot_narrow.phpt new file mode 100644 index 0000000000000..2d4f1f24671e7 --- /dev/null +++ b/Zend/tests/never-parameters/cannot_narrow.phpt @@ -0,0 +1,16 @@ +--TEST-- +`never` parameter types - `never` narrows the signature +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of WithNever::example(never $v) must be compatible with Base::example(string $v) in %s on line %d diff --git a/Zend/tests/never-parameters/must_be_method.phpt b/Zend/tests/never-parameters/must_be_method.phpt new file mode 100644 index 0000000000000..3997400b78dce --- /dev/null +++ b/Zend/tests/never-parameters/must_be_method.phpt @@ -0,0 +1,9 @@ +--TEST-- +`never` parameter types - can only be applied to class methods +--FILE-- + +--EXPECTF-- +Fatal error: never cannot be used as a parameter type for functions in %s on line %d diff --git a/Zend/tests/never-parameters/no_body.phpt b/Zend/tests/never-parameters/no_body.phpt new file mode 100644 index 0000000000000..f35f478cfd15b --- /dev/null +++ b/Zend/tests/never-parameters/no_body.phpt @@ -0,0 +1,14 @@ +--TEST-- +`never` parameter types - cannot be used for parameters on methods with bodies +--FILE-- + +--EXPECTF-- +Fatal error: Function Demo::example() containing a body cannot use never as a parameter type in %s on line %d diff --git a/Zend/tests/never-parameters/no_defaults.phpt b/Zend/tests/never-parameters/no_defaults.phpt new file mode 100644 index 0000000000000..f05c76bd378b5 --- /dev/null +++ b/Zend/tests/never-parameters/no_defaults.phpt @@ -0,0 +1,12 @@ +--TEST-- +`never` parameter types - cannot be used for parameters with defaults +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use int as default value for parameter $v of type never in %s on line %d diff --git a/Zend/tests/never-parameters/no_hooks.phpt b/Zend/tests/never-parameters/no_hooks.phpt new file mode 100644 index 0000000000000..c4f449c300fff --- /dev/null +++ b/Zend/tests/never-parameters/no_hooks.phpt @@ -0,0 +1,13 @@ +--TEST-- +`never` parameter types - cannot be used for hooked properties +--FILE-- + +--EXPECTF-- +Fatal error: Type of parameter $value of hook I::$both::set must be compatible with property type in %s on line %d diff --git a/Zend/tests/never-parameters/no_intersection.phpt b/Zend/tests/never-parameters/no_intersection.phpt new file mode 100644 index 0000000000000..45efadae12b26 --- /dev/null +++ b/Zend/tests/never-parameters/no_intersection.phpt @@ -0,0 +1,12 @@ +--TEST-- +`never` parameter types - cannot be part of an intersection +--FILE-- + +--EXPECTF-- +Fatal error: Type never cannot be part of an intersection type in %s on line %d diff --git a/Zend/tests/never-parameters/no_union.phpt b/Zend/tests/never-parameters/no_union.phpt new file mode 100644 index 0000000000000..99a646e593da4 --- /dev/null +++ b/Zend/tests/never-parameters/no_union.phpt @@ -0,0 +1,12 @@ +--TEST-- +`never` parameter types - cannot be part of a union +--FILE-- + +--EXPECTF-- +Fatal error: never can only be used as a standalone type in %s on line %d diff --git a/Zend/tests/never-parameters/reflection.phpt b/Zend/tests/never-parameters/reflection.phpt new file mode 100644 index 0000000000000..3fe2834998304 --- /dev/null +++ b/Zend/tests/never-parameters/reflection.phpt @@ -0,0 +1,26 @@ +--TEST-- +`never` parameter types - indicated via reflection +--FILE-- +getType(); + +assert($type instanceof ReflectionNamedType); +echo "Name: " . $type->getName() . "\n"; +echo "isBuiltin: " . $type->isBuiltIn() . "\n"; +echo "allowsNull: " . (int)$type->allowsNull() . "\n"; + +?> +--EXPECT-- +Parameter #0 [ never $v ] +Name: never +isBuiltin: 1 +allowsNull: 0 diff --git a/Zend/tests/never-parameters/untyped_implements.phpt b/Zend/tests/never-parameters/untyped_implements.phpt new file mode 100644 index 0000000000000..85dc2482974d9 --- /dev/null +++ b/Zend/tests/never-parameters/untyped_implements.phpt @@ -0,0 +1,42 @@ +--TEST-- +`never` parameter types - on interface, implementing class has no parameter type +--FILE-- +value; + } + public function set($v): void { + $this->value = $v; + } +} + +$box = new BoxedAnything(5); +var_dump($box); +var_dump($box->get()); +$box->set("testing"); +var_dump($box); +var_dump($box->get()); + +?> +--EXPECTF-- +object(BoxedAnything)#%d (1) { + ["value":"BoxedAnything":private]=> + int(5) +} +int(5) +object(BoxedAnything)#%d (1) { + ["value":"BoxedAnything":private]=> + string(7) "testing" +} +string(7) "testing" diff --git a/Zend/tests/never-parameters/untyped_subclass.phpt b/Zend/tests/never-parameters/untyped_subclass.phpt new file mode 100644 index 0000000000000..d6fe74d46e88d --- /dev/null +++ b/Zend/tests/never-parameters/untyped_subclass.phpt @@ -0,0 +1,42 @@ +--TEST-- +`never` parameter types - on abstract class, subclass has no parameter type +--FILE-- +value; + } + public function set($v): void { + $this->value = $v; + } +} + +$box = new BoxedAnything(5); +var_dump($box); +var_dump($box->get()); +$box->set("testing"); +var_dump($box); +var_dump($box->get()); + +?> +--EXPECTF-- +object(BoxedAnything)#%d (1) { + ["value":"BoxedAnything":private]=> + int(5) +} +int(5) +object(BoxedAnything)#%d (1) { + ["value":"BoxedAnything":private]=> + string(7) "testing" +} +string(7) "testing" diff --git a/Zend/tests/return_types/never_parameter.phpt b/Zend/tests/return_types/never_parameter.phpt deleted file mode 100644 index 7d1149d68f8ac..0000000000000 --- a/Zend/tests/return_types/never_parameter.phpt +++ /dev/null @@ -1,9 +0,0 @@ ---TEST-- -never return type: not valid as a parameter type ---FILE-- - ---EXPECTF-- -Fatal error: never cannot be used as a parameter type in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index e5df485919942..c70920237e9ec 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -7566,8 +7566,11 @@ static bool zend_property_is_virtual(zend_class_entry *ce, zend_string *property return is_virtual; } -static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fallback_return_type) /* {{{ */ +static void zend_compile_params(zend_ast_decl *decl, uint32_t fallback_return_type) /* {{{ */ { + zend_ast *ast = decl->child[0]; + zend_ast *return_type_ast = decl->child[3]; + zend_ast_list *list = zend_ast_get_list(ast); uint32_t i; zend_op_array *op_array = CG(active_op_array); @@ -7704,7 +7707,27 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 } if (ZEND_TYPE_FULL_MASK(arg_info->type) & MAY_BE_NEVER) { - zend_error_noreturn(E_COMPILE_ERROR, "never cannot be used as a parameter type"); + if (op_array->scope == NULL) { + zend_error_noreturn( + E_COMPILE_ERROR, + "never cannot be used as a parameter type for functions" + ); + } + if (decl->child[2] != NULL) { + zend_error_noreturn( + E_COMPILE_ERROR, + "Function %s::%s() containing a body cannot use never as a parameter type", + ZSTR_VAL(op_array->scope->name), + ZSTR_VAL(op_array->function_name) + ); + } + /* The restriction on not using `never` parameters for + * parameters with defaults is implemented by the validation of + * default values (since no value is valid for a `never` type). + * The restriction on not using `never` parameters for property + * hooks is implemented by the validation that the type of + * parameters accepted by the `set` hook is wider than that of + * the property itself. */ } if (default_type != IS_UNDEF && default_type != IS_CONSTANT_AST && !force_nullable @@ -8278,7 +8301,6 @@ static zend_op_array *zend_compile_func_decl_ex( zend_ast *params_ast = decl->child[0]; zend_ast *uses_ast = decl->child[1]; zend_ast *stmt_ast = decl->child[2]; - zend_ast *return_type_ast = decl->child[3]; bool is_method = decl->kind == ZEND_AST_METHOD; zend_string *lcname = NULL; bool is_hook = decl->kind == ZEND_AST_PROPERTY_HOOK; @@ -8380,7 +8402,7 @@ static zend_op_array *zend_compile_func_decl_ex( zend_stack_push(&CG(loop_var_stack), (void *) &dummy_var); } - zend_compile_params(params_ast, return_type_ast, + zend_compile_params(decl, is_method && zend_string_equals_literal(lcname, ZEND_TOSTRING_FUNC_NAME) ? IS_STRING : 0); if (CG(active_op_array)->fn_flags & ZEND_ACC_GENERATOR) { zend_mark_function_as_generator(); diff --git a/Zend/zend_enum.c b/Zend/zend_enum.c index ccafca48fe9b8..53cf3dcd08308 100644 --- a/Zend/zend_enum.c +++ b/Zend/zend_enum.c @@ -36,6 +36,30 @@ ZEND_API zend_class_entry *zend_ce_unit_enum; ZEND_API zend_class_entry *zend_ce_backed_enum; ZEND_API zend_object_handlers zend_enum_object_handlers; +/* We are going to need some argument info for ::from() and ::tryFrom() for + * enums that are int-backed or string-backed, we cannot just reuse the + * information from the BackedEnum interface's methods because those are typed + * with `never` parameters and for backed enums the type needs to match the + * backing type. These are not added to any actual class, just used for their + * type information. */ + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_IntBackedEnum_from, 0, 1, IS_STATIC, 0) + ZEND_ARG_TYPE_INFO(0, value, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_IntBackedEnum_tryFrom, 0, 1, IS_STATIC, 1) + ZEND_ARG_TYPE_INFO(0, value, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_StringBackedEnum_from, 0, 1, IS_STATIC, 0) + ZEND_ARG_TYPE_INFO(0, value, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_StringBackedEnum_tryFrom, 0, 1, IS_STATIC, 1) + ZEND_ARG_TYPE_INFO(0, value, IS_STRING, 0) +ZEND_END_ARG_INFO() + + zend_object *zend_enum_new(zval *result, zend_class_entry *ce, zend_string *case_name, zval *backing_value_zv) { zend_object *zobj = zend_objects_new(ce); @@ -456,7 +480,12 @@ void zend_enum_register_funcs(zend_class_entry *ce) from_function->doc_comment = NULL; from_function->num_args = 1; from_function->required_num_args = 1; - from_function->arg_info = (zend_internal_arg_info *) (arginfo_class_BackedEnum_from + 1); + if (ce->enum_backing_type == IS_LONG) { + from_function->arg_info = (zend_internal_arg_info *) (arginfo_class_IntBackedEnum_from + 1); + } else { + ZEND_ASSERT(ce->enum_backing_type == IS_STRING); + from_function->arg_info = (zend_internal_arg_info *) (arginfo_class_StringBackedEnum_from + 1); + } zend_enum_register_func(ce, ZEND_STR_FROM, from_function); zend_internal_function *try_from_function = zend_arena_calloc(&CG(arena), sizeof(zend_internal_function), 1); @@ -466,7 +495,12 @@ void zend_enum_register_funcs(zend_class_entry *ce) try_from_function->doc_comment = NULL; try_from_function->num_args = 1; try_from_function->required_num_args = 1; - try_from_function->arg_info = (zend_internal_arg_info *) (arginfo_class_BackedEnum_tryFrom + 1); + if (ce->enum_backing_type == IS_LONG) { + try_from_function->arg_info = (zend_internal_arg_info *) (arginfo_class_IntBackedEnum_tryFrom + 1); + } else { + ZEND_ASSERT(ce->enum_backing_type == IS_STRING); + try_from_function->arg_info = (zend_internal_arg_info *) (arginfo_class_StringBackedEnum_tryFrom + 1); + } zend_enum_register_func(ce, ZEND_STR_TRYFROM_LOWERCASE, try_from_function); } } @@ -493,10 +527,17 @@ static const zend_function_entry unit_enum_methods[] = { ZEND_FE_END }; -static const zend_function_entry backed_enum_methods[] = { +static const zend_function_entry int_backed_enum_methods[] = { + ZEND_NAMED_ME(cases, zend_enum_cases_func, arginfo_class_UnitEnum_cases, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_NAMED_ME(from, zend_enum_from_func, arginfo_class_IntBackedEnum_from, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_NAMED_ME(tryFrom, zend_enum_try_from_func, arginfo_class_IntBackedEnum_tryFrom, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_FE_END +}; + +static const zend_function_entry string_backed_enum_methods[] = { ZEND_NAMED_ME(cases, zend_enum_cases_func, arginfo_class_UnitEnum_cases, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_NAMED_ME(from, zend_enum_from_func, arginfo_class_BackedEnum_from, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - ZEND_NAMED_ME(tryFrom, zend_enum_try_from_func, arginfo_class_BackedEnum_tryFrom, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_NAMED_ME(from, zend_enum_from_func, arginfo_class_StringBackedEnum_from, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) + ZEND_NAMED_ME(tryFrom, zend_enum_try_from_func, arginfo_class_StringBackedEnum_tryFrom, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) ZEND_FE_END }; @@ -523,6 +564,9 @@ ZEND_API zend_class_entry *zend_register_internal_enum( ce, unit_enum_methods, &ce->function_table, EG(current_module)->type); zend_class_implements(ce, 1, zend_ce_unit_enum); } else { + const zend_function_entry *backed_enum_methods = type == IS_LONG + ? int_backed_enum_methods + : string_backed_enum_methods; zend_register_functions( ce, backed_enum_methods, &ce->function_table, EG(current_module)->type); zend_class_implements(ce, 1, zend_ce_backed_enum); diff --git a/Zend/zend_enum.stub.php b/Zend/zend_enum.stub.php index 727514a7bd4b7..07c0df6214bc8 100644 --- a/Zend/zend_enum.stub.php +++ b/Zend/zend_enum.stub.php @@ -9,7 +9,7 @@ public static function cases(): array; interface BackedEnum extends UnitEnum { - public static function from(int|string $value): static; + public static function from(never $value): static; - public static function tryFrom(int|string $value): ?static; + public static function tryFrom(never $value): ?static; } diff --git a/Zend/zend_enum_arginfo.h b/Zend/zend_enum_arginfo.h index 64c36ff3c33af..a5c1a0415ed01 100644 --- a/Zend/zend_enum_arginfo.h +++ b/Zend/zend_enum_arginfo.h @@ -1,15 +1,15 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 7092f1d4ba651f077cff37050899f090f00abf22 */ + * Stub hash: 2ff92bf440a77f97b89943b451a10d8154efbac4 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UnitEnum_cases, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_BackedEnum_from, 0, 1, IS_STATIC, 0) - ZEND_ARG_TYPE_MASK(0, value, MAY_BE_LONG|MAY_BE_STRING, NULL) + ZEND_ARG_TYPE_INFO(0, value, IS_NEVER, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_BackedEnum_tryFrom, 0, 1, IS_STATIC, 1) - ZEND_ARG_TYPE_MASK(0, value, MAY_BE_LONG|MAY_BE_STRING, NULL) + ZEND_ARG_TYPE_INFO(0, value, IS_NEVER, 0) ZEND_END_ARG_INFO() diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 807c902276a24..b4164c575d98b 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -771,6 +771,11 @@ static inheritance_status zend_do_perform_arg_type_hint_check( return INHERITANCE_SUCCESS; } + if (ZEND_TYPE_PURE_MASK(proto_arg_info->type) == MAY_BE_NEVER) { + /* Parent uses bottom type, always compatible */ + return INHERITANCE_SUCCESS; + } + if (!ZEND_TYPE_IS_SET(proto_arg_info->type)) { /* Child defines a type, but parent doesn't, violates LSP */ return INHERITANCE_ERROR;