diff --git a/Zend/tests/type_declarations/static_type_in_final_class.phpt b/Zend/tests/type_declarations/static_type_in_final_class.phpt new file mode 100644 index 0000000000000..2954d7d1ac9a1 --- /dev/null +++ b/Zend/tests/type_declarations/static_type_in_final_class.phpt @@ -0,0 +1,17 @@ +--TEST-- +While it may not be very useful, static is also permitted in final classes +--FILE-- + +--EXPECT-- +object(Test)#1 (0) { +} diff --git a/Zend/tests/type_declarations/static_type_outside_class.phpt b/Zend/tests/type_declarations/static_type_outside_class.phpt new file mode 100644 index 0000000000000..f485e394b4e48 --- /dev/null +++ b/Zend/tests/type_declarations/static_type_outside_class.phpt @@ -0,0 +1,10 @@ +--TEST-- +Static type outside class generates compile error +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use "static" when no class scope is active in %s on line %d diff --git a/Zend/tests/type_declarations/static_type_param.phpt b/Zend/tests/type_declarations/static_type_param.phpt new file mode 100644 index 0000000000000..4c0d6b1dfc9a9 --- /dev/null +++ b/Zend/tests/type_declarations/static_type_param.phpt @@ -0,0 +1,15 @@ +--TEST-- +Static type is not allowed in parameters +--FILE-- + +--EXPECTF-- +Parse error: syntax error, unexpected 'static' (T_STATIC), expecting variable (T_VARIABLE) in %s on line %d diff --git a/Zend/tests/type_declarations/static_type_property.phpt b/Zend/tests/type_declarations/static_type_property.phpt new file mode 100644 index 0000000000000..aba303d5fa225 --- /dev/null +++ b/Zend/tests/type_declarations/static_type_property.phpt @@ -0,0 +1,13 @@ +--TEST-- +Static type is not allowed in properties +--FILE-- + +--EXPECTF-- +Parse error: syntax error, unexpected 'static' (T_STATIC) in %s on line %d diff --git a/Zend/tests/type_declarations/static_type_return.phpt b/Zend/tests/type_declarations/static_type_return.phpt new file mode 100644 index 0000000000000..4a04cfff78b36 --- /dev/null +++ b/Zend/tests/type_declarations/static_type_return.phpt @@ -0,0 +1,91 @@ +--TEST-- +Static return type +--FILE-- +test1()); +var_dump($b->test1()); + +echo "\n"; +var_dump($a->test2()); +try { + var_dump($b->test2()); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} + +echo "\n"; +var_dump($a->test3()); +var_dump($b->test3()); + +echo "\n"; +var_dump($a->test4()); +try { + var_dump($b->test4()); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} + +echo "\n"; +$test = function($x): static { + return $x; +}; + +try { + var_dump($test(new stdClass)); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} + +$test = $test->bindTo($a); +var_dump($test($a)); + +?> +--EXPECT-- +object(A)#3 (0) { +} +object(B)#3 (0) { +} + +object(A)#3 (0) { +} +Return value of A::test2() must be an instance of B, instance of A returned + +object(A)#3 (0) { +} +object(C)#3 (0) { +} + +object(A)#3 (0) { +} +Return value of A::test4() must be of type B|array, instance of A returned + +Return value of {closure}() must be an instance of static, instance of stdClass returned +object(A)#1 (0) { +} diff --git a/Zend/tests/type_declarations/static_type_trait.phpt b/Zend/tests/type_declarations/static_type_trait.phpt new file mode 100644 index 0000000000000..ede9fd62b1e2c --- /dev/null +++ b/Zend/tests/type_declarations/static_type_trait.phpt @@ -0,0 +1,38 @@ +--TEST-- +static type in trait +--FILE-- +test($c)); +var_dump($c->test($p)); +var_dump($p->test($p)); +var_dump($p->test($c)); + +?> +--EXPECTF-- +object(C)#1 (0) { +} +object(P)#2 (0) { +} +object(P)#2 (0) { +} + +Fatal error: Uncaught TypeError: Return value of C::test() must be an instance of P, instance of C returned in %s:%d +Stack trace: +#0 %s(%d): C->test(Object(C)) +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/type_declarations/union_types/redundant_types/object_and_static.phpt b/Zend/tests/type_declarations/union_types/redundant_types/object_and_static.phpt new file mode 100644 index 0000000000000..f4152c2a234b1 --- /dev/null +++ b/Zend/tests/type_declarations/union_types/redundant_types/object_and_static.phpt @@ -0,0 +1,12 @@ +--TEST-- +object and static are redundant +--FILE-- + +--EXPECTF-- +Fatal error: Type static|object contains both object and a class type, which is redundant in %s on line %d diff --git a/Zend/tests/type_declarations/variance/static_variance_failure.phpt b/Zend/tests/type_declarations/variance/static_variance_failure.phpt new file mode 100644 index 0000000000000..159a69c30624c --- /dev/null +++ b/Zend/tests/type_declarations/variance/static_variance_failure.phpt @@ -0,0 +1,15 @@ +--TEST-- +Failure case for static variance: Replace static with self +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of B::test(): B must be compatible with A::test(): static in %s on line %d diff --git a/Zend/tests/type_declarations/variance/static_variance_success.phpt b/Zend/tests/type_declarations/variance/static_variance_success.phpt new file mode 100644 index 0000000000000..3749d8d874c7e --- /dev/null +++ b/Zend/tests/type_declarations/variance/static_variance_success.phpt @@ -0,0 +1,25 @@ +--TEST-- +Success cases for static variance +--FILE-- + +===DONE=== +--EXPECT-- +===DONE=== diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index d43bb6f4a34f0..58fb3377d2cfc 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1196,6 +1196,16 @@ zend_string *zend_type_to_string_resolved(zend_type type, zend_class_entry *scop } uint32_t type_mask = ZEND_TYPE_FULL_MASK(type); + if (type_mask & MAY_BE_STATIC) { + zend_string *name = ZSTR_KNOWN(ZEND_STR_STATIC); + if (scope) { + zend_class_entry *called_scope = zend_get_called_scope(EG(current_execute_data)); + if (called_scope) { + name = called_scope->name; + } + } + str = add_type_string(str, name); + } if (type_mask & MAY_BE_CALLABLE) { str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_CALLABLE)); } @@ -5502,6 +5512,10 @@ static zend_type zend_compile_single_typename(zend_ast *ast) { ZEND_ASSERT(!(ast->attr & ZEND_TYPE_NULLABLE)); if (ast->kind == ZEND_AST_TYPE) { + if (ast->attr == IS_STATIC && !CG(active_class_entry) && zend_is_scope_known()) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot use \"static\" when no class scope is active"); + } return (zend_type) ZEND_TYPE_INIT_CODE(ast->attr, 0, 0); } else { zend_string *class_name = zend_ast_get_str(ast); @@ -5657,7 +5671,7 @@ static zend_type zend_compile_typename( ZSTR_VAL(type_str)); } - if ((type_mask & MAY_BE_OBJECT) && ZEND_TYPE_HAS_CLASS(type)) { + if ((type_mask & MAY_BE_OBJECT) && (ZEND_TYPE_HAS_CLASS(type) || (type_mask & MAY_BE_STATIC))) { zend_string *type_str = zend_type_to_string(type); zend_error_noreturn(E_COMPILE_ERROR, "Type %s contains both object and a class type, which is redundant", diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 140038e4c333a..3fbbcef0feb67 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -674,7 +674,7 @@ static ZEND_COLD void zend_verify_type_error_common( } if (is_union_type(arg_info->type)) { - zend_string *type_str = zend_type_to_string(arg_info->type); + zend_string *type_str = zend_type_to_string_resolved(arg_info->type, zf->common.scope); smart_str_appends(&str, "be of type "); smart_str_append(&str, type_str); zend_string_release(type_str); @@ -714,6 +714,16 @@ static ZEND_COLD void zend_verify_type_error_common( case MAY_BE_ITERABLE: smart_str_appends(&str, "be iterable"); break; + case MAY_BE_STATIC: { + zend_class_entry *called_scope = zend_get_called_scope(EG(current_execute_data)); + smart_str_appends(&str, "be an instance of "); + if (called_scope) { + smart_str_append(&str, called_scope->name); + } else { + smart_str_appends(&str, "static"); + } + break; + } default: { /* Hack to print the type without null */ @@ -735,7 +745,9 @@ static ZEND_COLD void zend_verify_type_error_common( *need_msg = smart_str_extract(&str); if (value) { - if (ZEND_TYPE_HAS_CLASS(arg_info->type) && Z_TYPE_P(value) == IS_OBJECT) { + zend_bool has_class = ZEND_TYPE_HAS_CLASS(arg_info->type) + || (ZEND_TYPE_FULL_MASK(arg_info->type) & MAY_BE_STATIC); + if (has_class && Z_TYPE_P(value) == IS_OBJECT) { *given_msg = "instance of "; *given_kind = ZSTR_VAL(Z_OBJCE_P(value)->name); } else { @@ -988,11 +1000,12 @@ static zend_always_inline zend_bool i_zend_check_property_type(zend_property_inf return 1; } - ZEND_ASSERT(!(ZEND_TYPE_FULL_MASK(info->type) & MAY_BE_CALLABLE)); - if ((ZEND_TYPE_FULL_MASK(info->type) & MAY_BE_ITERABLE) && zend_is_iterable(property)) { + uint32_t type_mask = ZEND_TYPE_FULL_MASK(info->type); + ZEND_ASSERT(!(type_mask & (MAY_BE_CALLABLE|MAY_BE_STATIC))); + if ((type_mask & MAY_BE_ITERABLE) && zend_is_iterable(property)) { return 1; } - return zend_verify_scalar_type_hint(ZEND_TYPE_FULL_MASK(info->type), property, strict, 0); + return zend_verify_scalar_type_hint(type_mask, property, strict, 0); } static zend_always_inline zend_bool i_zend_verify_property_type(zend_property_info *info, zval *property, zend_bool strict) @@ -1024,6 +1037,19 @@ static zend_never_inline zval* zend_assign_to_typed_prop(zend_property_info *inf return zend_assign_to_variable(property_val, &tmp, IS_TMP_VAR, EX_USES_STRICT_TYPES()); } +ZEND_API zend_bool zend_value_instanceof_static(zval *zv) { + if (Z_TYPE_P(zv) != IS_OBJECT) { + return 0; + } + + zend_class_entry *called_scope = zend_get_called_scope(EG(current_execute_data)); + if (!called_scope) { + return 0; + } + return instanceof_function(Z_OBJCE_P(zv), called_scope); +} + + static zend_always_inline zend_bool zend_check_type_slow( zend_type type, zval *arg, zend_reference *ref, void **cache_slot, zend_class_entry *scope, zend_bool is_return_type, zend_bool is_internal) @@ -1074,6 +1100,9 @@ static zend_always_inline zend_bool zend_check_type_slow( if ((type_mask & MAY_BE_ITERABLE) && zend_is_iterable(arg)) { return 1; } + if ((type_mask & MAY_BE_STATIC) && zend_value_instanceof_static(arg)) { + return 1; + } if (ref && ZEND_REF_HAS_TYPE_SOURCES(ref)) { /* We cannot have conversions for typed refs. */ return 0; @@ -3046,7 +3075,7 @@ static zend_always_inline int i_zend_verify_type_assignable_zval( } type_mask = ZEND_TYPE_FULL_MASK(type); - ZEND_ASSERT(!(type_mask & MAY_BE_CALLABLE)); + ZEND_ASSERT(!(type_mask & (MAY_BE_CALLABLE|MAY_BE_STATIC))); if (type_mask & MAY_BE_ITERABLE) { return zend_is_iterable(zv); } diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index 44b48e914c3d7..f29a5eb050229 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -67,6 +67,8 @@ ZEND_API ZEND_COLD void zend_verify_arg_error( ZEND_API ZEND_COLD void zend_verify_return_error( const zend_function *zf, void **cache_slot, zval *value); ZEND_API zend_bool zend_verify_ref_array_assignable(zend_reference *ref); +ZEND_API zend_bool zend_value_instanceof_static(zval *zv); + #define ZEND_REF_TYPE_SOURCES(ref) \ (ref)->sources diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 5abe1c55d4a0c..219c1ffcc9f06 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -333,6 +333,28 @@ static zend_bool zend_type_contains_traversable(zend_type type) { return 0; } +static zend_bool zend_type_permits_self( + zend_type type, zend_class_entry *scope, zend_class_entry *self) { + if (ZEND_TYPE_FULL_MASK(type) & MAY_BE_OBJECT) { + return 1; + } + + /* Any types that may satisfy self must have already been loaded at this point + * (as a parent or interface), so we never need to register delayed variance obligations + * for this case. */ + zend_type *single_type; + ZEND_TYPE_FOREACH(type, single_type) { + if (ZEND_TYPE_HAS_NAME(*single_type)) { + zend_string *name = resolve_class_name(scope, ZEND_TYPE_NAME(*single_type)); + zend_class_entry *ce = lookup_class(self, name, /* register_unresolved */ 0); + if (ce && unlinked_instanceof(self, ce)) { + return 1; + } + } + } ZEND_TYPE_FOREACH_END(); + return 0; +} + /* Unresolved means that class declarations that are currently not available are needed to * determine whether the inheritance is valid or not. At runtime UNRESOLVED should be treated * as an ERROR. */ @@ -406,13 +428,22 @@ static inheritance_status zend_perform_covariant_type_check( if (added_types) { // TODO: Make "iterable" an alias of "array|Traversable" instead, // so these special cases will be handled automatically. - if (added_types == MAY_BE_ITERABLE + if ((added_types & MAY_BE_ITERABLE) && (proto_type_mask & MAY_BE_ARRAY) && zend_type_contains_traversable(proto_type)) { /* Replacing array|Traversable with iterable is okay */ - } else if (added_types == MAY_BE_ARRAY && (proto_type_mask & MAY_BE_ITERABLE)) { + added_types &= ~MAY_BE_ITERABLE; + } + if ((added_types & MAY_BE_ARRAY) && (proto_type_mask & MAY_BE_ITERABLE)) { /* Replacing iterable with array is okay */ - } else { + added_types &= ~MAY_BE_ARRAY; + } + if ((added_types & MAY_BE_STATIC) + && zend_type_permits_self(proto_type, proto_scope, fe_scope)) { + /* Replacing type that accepts self with static is okay */ + added_types &= ~MAY_BE_STATIC; + } + if (added_types) { /* Otherwise adding new types is illegal */ return INHERITANCE_ERROR; } diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 02dbd091dcf89..c1ced9a35aa21 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -235,7 +235,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type unprefixed_use_declarations const_decl inner_statement %type expr optional_expr while_statement for_statement foreach_variable %type foreach_statement declare_statement finally_statement unset_variable variable -%type extends_from parameter optional_type argument global_var +%type extends_from parameter optional_type_without_static argument global_var %type static_var class_statement trait_adaptation trait_precedence trait_alias %type absolute_trait_method_reference trait_method_reference property echo_expr %type new_expr anonymous_class class_name class_name_reference simple_variable @@ -254,8 +254,8 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type ctor_arguments alt_if_stmt_without_else trait_adaptation_list lexical_vars %type lexical_var_list encaps_list %type array_pair non_empty_array_pair_list array_pair_list possible_array_pair -%type isset_variable type return_type type_expr -%type identifier +%type isset_variable type return_type type_expr type_without_static +%type identifier type_expr_without_static union_type_without_static %type inline_function union_type %type returns_ref function fn is_reference is_variadic variable_modifiers @@ -646,28 +646,27 @@ non_empty_parameter_list: ; parameter: - optional_type is_reference is_variadic T_VARIABLE + optional_type_without_static is_reference is_variadic T_VARIABLE { $$ = zend_ast_create_ex(ZEND_AST_PARAM, $2 | $3, $1, $4, NULL); } - | optional_type is_reference is_variadic T_VARIABLE '=' expr + | optional_type_without_static is_reference is_variadic T_VARIABLE '=' expr { $$ = zend_ast_create_ex(ZEND_AST_PARAM, $2 | $3, $1, $4, $6); } ; -optional_type: +optional_type_without_static: %empty { $$ = NULL; } - | type_expr { $$ = $1; } + | type_expr_without_static { $$ = $1; } ; type_expr: - type { $$ = $1; } - | '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } - | union_type { $$ = $1; } + type { $$ = $1; } + | '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } + | union_type { $$ = $1; } ; type: - T_ARRAY { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_ARRAY); } - | T_CALLABLE { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_CALLABLE); } - | name { $$ = $1; } + type_without_static { $$ = $1; } + | T_STATIC { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_STATIC); } ; union_type: @@ -675,6 +674,28 @@ union_type: | union_type '|' type { $$ = zend_ast_list_add($1, $3); } ; +/* Duplicate the type rules without "static", + * to avoid conflicts with "static" modifier for properties. */ + +type_expr_without_static: + type_without_static { $$ = $1; } + | '?' type_without_static { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } + | union_type_without_static { $$ = $1; } +; + +type_without_static: + T_ARRAY { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_ARRAY); } + | T_CALLABLE { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_CALLABLE); } + | name { $$ = $1; } +; + +union_type_without_static: + type_without_static '|' type_without_static + { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $1, $3); } + | union_type_without_static '|' type_without_static + { $$ = zend_ast_list_add($1, $3); } +; + return_type: %empty { $$ = NULL; } | ':' type_expr { $$ = $2; } @@ -728,7 +749,7 @@ class_statement_list: class_statement: - variable_modifiers optional_type property_list ';' + variable_modifiers optional_type_without_static property_list ';' { $$ = zend_ast_create(ZEND_AST_PROP_GROUP, $2, $3); $$->attr = $1; } | method_modifiers T_CONST class_const_list ';' diff --git a/Zend/zend_type_info.h b/Zend/zend_type_info.h index 752e6a4b850e9..bace8014bfc3f 100644 --- a/Zend/zend_type_info.h +++ b/Zend/zend_type_info.h @@ -40,6 +40,7 @@ #define MAY_BE_CALLABLE (1 << IS_CALLABLE) #define MAY_BE_ITERABLE (1 << IS_ITERABLE) #define MAY_BE_VOID (1 << IS_VOID) +#define MAY_BE_STATIC (1 << IS_STATIC) #define MAY_BE_ARRAY_SHIFT (IS_REFERENCE) diff --git a/Zend/zend_types.h b/Zend/zend_types.h index 1b314afa24287..d8b2280e47f8b 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -533,6 +533,7 @@ struct _zend_ast_ref { #define IS_CALLABLE 12 #define IS_ITERABLE 13 #define IS_VOID 14 +#define IS_STATIC 15 /* internal types */ #define IS_INDIRECT 12 diff --git a/ext/opcache/Optimizer/dfa_pass.c b/ext/opcache/Optimizer/dfa_pass.c index 8802577154269..e27826b6327b2 100644 --- a/ext/opcache/Optimizer/dfa_pass.c +++ b/ext/opcache/Optimizer/dfa_pass.c @@ -307,7 +307,7 @@ static inline zend_bool can_elide_return_type_check( } /* These types are not represented exactly */ - if (ZEND_TYPE_FULL_MASK(info->type) & (MAY_BE_CALLABLE|MAY_BE_ITERABLE)) { + if (ZEND_TYPE_FULL_MASK(info->type) & (MAY_BE_CALLABLE|MAY_BE_ITERABLE|MAY_BE_STATIC)) { return 0; } diff --git a/ext/opcache/Optimizer/zend_inference.c b/ext/opcache/Optimizer/zend_inference.c index 90273e2ffdc2d..4675cfe99b8f9 100644 --- a/ext/opcache/Optimizer/zend_inference.c +++ b/ext/opcache/Optimizer/zend_inference.c @@ -2223,6 +2223,9 @@ static uint32_t zend_convert_type_declaration_mask(uint32_t type_mask) { if (type_mask & MAY_BE_ITERABLE) { result_mask |= MAY_BE_OBJECT|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF; } + if (type_mask & MAY_BE_STATIC) { + result_mask |= MAY_BE_OBJECT; + } if (type_mask & MAY_BE_ARRAY) { result_mask |= MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF; } diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c index d0caabe5da19b..7fd91c7317d4b 100644 --- a/ext/opcache/jit/zend_jit_helpers.c +++ b/ext/opcache/jit/zend_jit_helpers.c @@ -1190,6 +1190,9 @@ static zend_always_inline zend_bool zend_jit_verify_type_common(zval *arg, const if ((type_mask & MAY_BE_ITERABLE) && zend_is_iterable(arg)) { return 1; } + if ((type_mask & MAY_BE_STATIC) && zend_value_instanceof_static(arg)) { + return 1; + } if (zend_verify_scalar_type_hint(type_mask, arg, ZEND_ARG_USES_STRICT_TYPES(), /* is_internal */ 0)) { return 1; } diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 95c4859e94cec..8d2d53b0286d1 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -2909,7 +2909,9 @@ ZEND_METHOD(reflection_named_type, isBuiltin) } GET_REFLECTION_OBJECT_PTR(param); - RETVAL_BOOL(ZEND_TYPE_IS_ONLY_MASK(param->type)); + /* Treat "static" as a class type for the purposes of reflection. */ + RETVAL_BOOL(ZEND_TYPE_IS_ONLY_MASK(param->type) + && !(ZEND_TYPE_FULL_MASK(param->type) & MAY_BE_STATIC)); } /* }}} */ diff --git a/ext/reflection/tests/static_type.phpt b/ext/reflection/tests/static_type.phpt new file mode 100644 index 0000000000000..b541008f795b6 --- /dev/null +++ b/ext/reflection/tests/static_type.phpt @@ -0,0 +1,22 @@ +--TEST-- +ReflectionType for static types +--FILE-- +getReturnType(); +var_dump($rt->isBuiltin()); +var_dump($rt->getName()); +var_dump((string) $rt); + +?> +--EXPECT-- +bool(false) +string(6) "static" +string(6) "static"