From 0dc1c19da9123838622f945bd38c569ce26c4ba8 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 09:35:26 +0100 Subject: [PATCH 01/26] Add a test that fails --- .../inner_classes/simple_declaration.phpt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/classes/inner_classes/simple_declaration.phpt diff --git a/tests/classes/inner_classes/simple_declaration.phpt b/tests/classes/inner_classes/simple_declaration.phpt new file mode 100644 index 0000000000000..93e9b16f4b241 --- /dev/null +++ b/tests/classes/inner_classes/simple_declaration.phpt @@ -0,0 +1,21 @@ +--TEST-- +simple declaration +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) From 8905724aee95d30f2b843c09c9eab6371162bec3 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 09:42:25 +0100 Subject: [PATCH 02/26] Add scope to class entries The idea here is to create something that can be used for other things essentially making this quite flexible. required scope: when defined, the class can only be used in the required scope, or its descendents when absolute is false lexical scope: the scope the class is defined in -- either a namespace or another class. --- Zend/zend.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Zend/zend.h b/Zend/zend.h index 0cf1faeb653fe..bb05c40a2ffe1 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -164,6 +164,10 @@ struct _zend_class_entry { HashTable properties_info; HashTable constants_table; + zend_class_entry *required_scope; + zend_class_entry *lexical_scope; + bool required_scope_absolute; + ZEND_MAP_PTR_DEF(zend_class_mutable_data*, mutable_data); zend_inheritance_cache_entry *inheritance_cache; From be18661883b48c7faddde5e98ecc758a07c4261a Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 09:52:03 +0100 Subject: [PATCH 03/26] modify grammar Now we modify the grammar to allow specifying visibility on a class. --- Zend/zend_compile.c | 40 ++++++++++++++++++++++++++++++++++--- Zend/zend_compile.h | 1 + Zend/zend_language_parser.y | 17 ++++++++++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 8e4221673c4cf..e5a3b4e6f413c 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -893,12 +893,12 @@ uint32_t zend_modifier_token_to_flag(zend_modifier_target target, uint32_t token } break; case T_READONLY: - if (target == ZEND_MODIFIER_TARGET_PROPERTY || target == ZEND_MODIFIER_TARGET_CPP) { + if (target == ZEND_MODIFIER_TARGET_PROPERTY || target == ZEND_MODIFIER_TARGET_CPP || target == ZEND_MODIFIER_TARGET_NESTED_CLASS) { return ZEND_ACC_READONLY; } break; case T_ABSTRACT: - if (target == ZEND_MODIFIER_TARGET_METHOD || target == ZEND_MODIFIER_TARGET_PROPERTY) { + if (target == ZEND_MODIFIER_TARGET_METHOD || target == ZEND_MODIFIER_TARGET_PROPERTY || target == ZEND_MODIFIER_TARGET_NESTED_CLASS) { return ZEND_ACC_ABSTRACT; } break; @@ -906,7 +906,8 @@ uint32_t zend_modifier_token_to_flag(zend_modifier_target target, uint32_t token if (target == ZEND_MODIFIER_TARGET_METHOD || target == ZEND_MODIFIER_TARGET_CONSTANT || target == ZEND_MODIFIER_TARGET_PROPERTY - || target == ZEND_MODIFIER_TARGET_PROPERTY_HOOK) { + || target == ZEND_MODIFIER_TARGET_PROPERTY_HOOK + || target == ZEND_MODIFIER_TARGET_NESTED_CLASS) { return ZEND_ACC_FINAL; } break; @@ -943,6 +944,8 @@ uint32_t zend_modifier_token_to_flag(zend_modifier_target target, uint32_t token member = "parameter"; } else if (target == ZEND_MODIFIER_TARGET_PROPERTY_HOOK) { member = "property hook"; + } else if (target == ZEND_MODIFIER_TARGET_NESTED_CLASS) { + member = "nested class"; } else { ZEND_UNREACHABLE(); } @@ -1050,6 +1053,37 @@ uint32_t zend_add_member_modifier(uint32_t flags, uint32_t new_flag, zend_modifi return 0; } } + if (target == ZEND_MODIFIER_TARGET_NESTED_CLASS) { + if ((flags & ZEND_ACC_PPP_MASK) && (new_flag & ZEND_ACC_PPP_MASK)) { + zend_throw_exception(zend_ce_compile_error, + "Multiple access type modifiers are not allowed", 0); + return 0; + } + + if ((flags & ZEND_ACC_STATIC) || (new_flag & ZEND_ACC_STATIC)) { + zend_throw_exception(zend_ce_compile_error, + "Static inner classes are not allowed", 0); + return 0; + } + + if ((flags & ZEND_ACC_PUBLIC_SET) || (new_flag & ZEND_ACC_PUBLIC_SET)) { + zend_throw_exception(zend_ce_compile_error, + "Public(set) inner classes are not allowed", 0); + return 0; + } + + if ((flags & ZEND_ACC_PROTECTED_SET) || (new_flag & ZEND_ACC_PROTECTED_SET)) { + zend_throw_exception(zend_ce_compile_error, + "Protected(set) inner classes are not allowed", 0); + return 0; + } + + if ((flags & ZEND_ACC_PRIVATE_SET) || (new_flag & ZEND_ACC_PRIVATE_SET)) { + zend_throw_exception(zend_ce_compile_error, + "Private(set) inner classes are not allowed", 0); + return 0; + } + } return new_flags; } /* }}} */ diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 224a68be749cb..9271a26c3980f 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -894,6 +894,7 @@ typedef enum { ZEND_MODIFIER_TARGET_CONSTANT, ZEND_MODIFIER_TARGET_CPP, ZEND_MODIFIER_TARGET_PROPERTY_HOOK, + ZEND_MODIFIER_TARGET_NESTED_CLASS, } zend_modifier_target; /* Used during AST construction */ diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 9483a83b4e955..3c8785477971b 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -285,10 +285,10 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type enum_declaration_statement enum_backing_type enum_case enum_case_expr %type function_name non_empty_member_modifiers %type property_hook property_hook_list optional_property_hook_list hooked_property property_hook_body -%type optional_parameter_list +%type optional_parameter_list nested_class_statement %type returns_ref function fn is_reference is_variadic property_modifiers property_hook_modifiers -%type method_modifiers class_const_modifiers member_modifier optional_cpp_modifiers +%type method_modifiers class_const_modifiers member_modifier optional_cpp_modifiers nested_class_modifiers %type class_modifiers class_modifier anonymous_class_modifiers anonymous_class_modifiers_optional use_type backup_fn_flags %type backup_lex_pos @@ -628,6 +628,14 @@ class_modifier: | T_READONLY { $$ = ZEND_ACC_READONLY_CLASS|ZEND_ACC_NO_DYNAMIC_PROPERTIES; } ; +nested_class_modifiers: + non_empty_member_modifiers + { $$ = zend_modifier_list_to_flags(ZEND_MODIFIER_TARGET_NESTED_CLASS, $1); + if (!$$) { YYERROR; } } + | %empty + { $$ = ZEND_ACC_PUBLIC; } +; + trait_declaration_statement: T_TRAIT { $$ = CG(zend_lineno); } T_STRING backup_doc_comment '{' class_statement_list '}' @@ -943,6 +951,10 @@ class_statement_list: { $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); } ; +nested_class_statement: + T_CLASS T_STRING { $$ = CG(zend_lineno); } extends_from implements_list backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $3, $6, zend_ast_get_str($2), $4, $5, $8, NULL, NULL); } +; attributed_class_statement: property_modifiers optional_type_without_static property_list ';' @@ -962,6 +974,7 @@ attributed_class_statement: { $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1 | $12, $2, $5, zend_ast_get_str($4), $7, NULL, $11, $9, NULL); CG(extra_fn_flags) = $10; } | enum_case { $$ = $1; } + | nested_class_modifiers nested_class_statement { $$ = $2; $$->attr = $1; } ; class_statement: From d382a7f85dbda4b7205bb42615ba1c3f3a7f0f82 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 10:48:44 +0100 Subject: [PATCH 04/26] namespaces: add support This adds a new EG to support the addition of using namespaces as a class's lexical scope --- Zend/zend_compile.h | 1 + Zend/zend_globals.h | 2 + Zend/zend_namespaces.c | 95 ++++++++++++++++++++++++++++++++++++++++++ Zend/zend_namespaces.h | 28 +++++++++++++ configure.ac | 1 + win32/build/config.w32 | 2 +- 6 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 Zend/zend_namespaces.c create mode 100644 Zend/zend_namespaces.h diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 9271a26c3980f..50dc535d95f74 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -1061,6 +1061,7 @@ ZEND_API zend_string *zend_type_to_string(zend_type type); #define ZEND_INTERNAL_CLASS 1 #define ZEND_USER_CLASS 2 +#define ZEND_NAMESPACE_CLASS 3 #define ZEND_EVAL (1<<0) #define ZEND_INCLUDE (1<<1) diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 079bfb99caccf..b8422413c9c65 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -191,6 +191,8 @@ struct _zend_executor_globals { HashTable *function_table; /* function symbol table */ HashTable *class_table; /* class table */ HashTable *zend_constants; /* constants table */ + HashTable *namespaces; /* namespace table */ + zend_class_entry *global_namespace; zval *vm_stack_top; zval *vm_stack_end; diff --git a/Zend/zend_namespaces.c b/Zend/zend_namespaces.c new file mode 100644 index 0000000000000..a7d9093357fca --- /dev/null +++ b/Zend/zend_namespaces.c @@ -0,0 +1,95 @@ +/* ++----------------------------------------------------------------------+ + | Zend Engine | + +----------------------------------------------------------------------+ + | Copyright (c) Zend Technologies Ltd. (http://www.zend.com) | + +----------------------------------------------------------------------+ + | This source file is subject to version 2.00 of the Zend license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.zend.com/license/2_00.txt. | + | If you did not receive a copy of the Zend license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@zend.com so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Rob Landers | + | | + +----------------------------------------------------------------------+ +*/ + +#include "zend_namespaces.h" +#include "zend_API.h" +#include "zend_hash.h" + +zend_class_entry *create_namespace(zend_string *name) { + zend_class_entry *ns = pemalloc(sizeof(zend_class_entry), 0); + zend_initialize_class_data(ns, 1); + ns->type = ZEND_NAMESPACE_CLASS; + ns->ce_flags |= ZEND_ACC_UNINSTANTIABLE; + ns->name = zend_string_copy(name); + + return ns; +} + +static zend_class_entry *insert_namespace(const zend_string *name) { + zend_class_entry *parent_ns = EG(global_namespace); + zend_class_entry *ns = parent_ns; + const char *start = ZSTR_VAL(name); + const char *end = start + ZSTR_LEN(name); + const char *pos = start; + size_t len = 0; + + while (pos < end) { + if (*pos == '\\') { + len = pos - start; + zend_string *needle = zend_string_init(ZSTR_VAL(name), len, 0); + + ns = zend_hash_find_ptr(EG(namespaces), needle); + + if (!ns) { + zend_string *interned_name = zend_new_interned_string(needle); + ns = create_namespace(interned_name); + ns->lexical_scope = parent_ns; + zend_hash_add_ptr(EG(namespaces), interned_name, ns); + } + zend_string_release(needle); + + parent_ns = ns; + } + pos ++; + } + + return ns; +} + +zend_class_entry *zend_resolve_namespace(zend_string *name) { + if (EG(global_namespace) == NULL) { + EG(global_namespace) = create_namespace(zend_empty_string); + EG(global_namespace)->lexical_scope = NULL; + zend_hash_init(EG(namespaces), 8, NULL, ZEND_CLASS_DTOR, 0); + zend_hash_add_ptr(EG(namespaces), zend_empty_string, EG(global_namespace)); + } + + if (name == NULL || ZSTR_LEN(name) == 0) { + return EG(global_namespace); + } + + zend_string *lc_name = zend_string_tolower(name); + zend_class_entry *ns = zend_hash_find_ptr(EG(namespaces), lc_name); + + if (!ns) { + ns = insert_namespace(lc_name); + } + + zend_string_release(lc_name); + + return ns; +} + +zend_class_entry *zend_lookup_namespace(zend_string *name) { + zend_string *lc_name = zend_string_tolower(name); + zend_class_entry *ns = zend_hash_find_ptr(EG(namespaces), lc_name); + zend_string_release(lc_name); + + return ns; +} diff --git a/Zend/zend_namespaces.h b/Zend/zend_namespaces.h new file mode 100644 index 0000000000000..a1daaf839d29d --- /dev/null +++ b/Zend/zend_namespaces.h @@ -0,0 +1,28 @@ +/* ++----------------------------------------------------------------------+ + | Zend Engine | + +----------------------------------------------------------------------+ + | Copyright (c) Zend Technologies Ltd. (http://www.zend.com) | + +----------------------------------------------------------------------+ + | This source file is subject to version 2.00 of the Zend license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.zend.com/license/2_00.txt. | + | If you did not receive a copy of the Zend license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@zend.com so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Rob Landers | + | | + +----------------------------------------------------------------------+ +*/ + +#ifndef ZEND_NAMESPACES_H +#define ZEND_NAMESPACES_H + +#include "zend_compile.h" + +ZEND_API zend_class_entry *zend_resolve_namespace(zend_string *name); +ZEND_API zend_class_entry *zend_lookup_namespace(zend_string *name); + +#endif //ZEND_NAMESPACES_H diff --git a/configure.ac b/configure.ac index 01d9ded69b920..e1f99ab8e287d 100644 --- a/configure.ac +++ b/configure.ac @@ -1757,6 +1757,7 @@ PHP_ADD_SOURCES([Zend], m4_normalize([ zend_opcode.c zend_operators.c zend_property_hooks.c + zend_namespaces.c zend_ptr_stack.c zend_signal.c zend_smart_str.c diff --git a/win32/build/config.w32 b/win32/build/config.w32 index f82ed73efe3bd..85efc27c6fb4f 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -240,7 +240,7 @@ ADD_SOURCES("Zend", "zend_language_parser.c zend_language_scanner.c \ zend_default_classes.c zend_execute.c zend_strtod.c zend_gc.c zend_closures.c zend_weakrefs.c \ zend_float.c zend_string.c zend_generators.c zend_virtual_cwd.c zend_ast.c \ zend_inheritance.c zend_smart_str.c zend_cpuinfo.c zend_observer.c zend_system_id.c \ - zend_enum.c zend_fibers.c zend_atomic.c zend_hrtime.c zend_frameless_function.c zend_property_hooks.c \ + zend_enum.c zend_fibers.c zend_atomic.c zend_hrtime.c zend_frameless_function.c zend_property_hooks.c zend_namespaces.c \ zend_lazy_objects.c"); ADD_SOURCES("Zend\\Optimizer", "zend_optimizer.c pass1.c pass3.c optimize_func_calls.c block_pass.c optimize_temp_vars_5.c nop_removal.c compact_literals.c zend_cfg.c zend_dfg.c dfa_pass.c zend_ssa.c zend_inference.c zend_func_info.c zend_call_graph.c zend_dump.c escape_analysis.c compact_vars.c dce.c sccp.c scdf.c"); From 05686a2361cd786093d0b7559b232aac4c2be5af Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 12:17:11 +0100 Subject: [PATCH 05/26] allow for defining nested classes Now we can define nested classes inside other classes --- Zend/zend_compile.c | 176 ++++++++++++++++++++++++++++++++++++---- Zend/zend_execute_API.c | 5 ++ Zend/zend_globals.h | 1 + Zend/zend_namespaces.c | 9 ++ Zend/zend_namespaces.h | 1 + 5 files changed, 177 insertions(+), 15 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index e5a3b4e6f413c..8b32d9d88b35e 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -38,6 +38,7 @@ #include "zend_call_stack.h" #include "zend_frameless_function.h" #include "zend_property_hooks.h" +#include "zend_namespaces.h" #define SET_NODE(target, src) do { \ target ## _type = (src)->op_type; \ @@ -424,6 +425,7 @@ void zend_init_compiler_data_structures(void) /* {{{ */ zend_stack_init(&CG(delayed_oplines_stack), sizeof(zend_op)); zend_stack_init(&CG(short_circuiting_opnums), sizeof(uint32_t)); CG(active_class_entry) = NULL; + CG(nested_class_queue) = NULL; CG(in_compilation) = 0; CG(skip_shebang) = 0; @@ -1178,6 +1180,60 @@ static zend_string *zend_resolve_const_name(zend_string *name, uint32_t type, bo name, type, is_fully_qualified, 1, FC(imports_const)); } +static zend_string *get_namespace_from_scope(const zend_class_entry *scope) +{ + ZEND_ASSERT(scope != NULL); + while (scope && scope->lexical_scope && scope->type != ZEND_NAMESPACE_CLASS) { + scope = scope->lexical_scope; + } + return zend_string_copy(scope->name); +} + +static zend_string *get_scoped_name(zend_string *ns, zend_string *name) +{ + name = zend_string_tolower(name); + if (ns && ZSTR_LEN(ns) && ZSTR_LEN(name) > ZSTR_LEN(ns) + 1 && + memcmp(ZSTR_VAL(name), ZSTR_VAL(ns), ZSTR_LEN(ns)) == 0 && + ZSTR_VAL(name)[ZSTR_LEN(ns)] == '\\') { + zend_string *ret = zend_string_init(ZSTR_VAL(name) + ZSTR_LEN(ns) + 1, ZSTR_LEN(name) - ZSTR_LEN(ns) - 1, 0); + zend_string_release(name); + return ret; + } + return name; +} + +zend_string *zend_resolve_class_in_scope(zend_string *name, const zend_class_entry *scope) +{ + zend_string *ns_name = get_namespace_from_scope(scope); + zend_string *original_suffix = get_scoped_name(ns_name, name); + zend_string_release(ns_name); + + const zend_class_entry *current_scope = scope; + + while (current_scope && current_scope->type != ZEND_NAMESPACE_CLASS) { + zend_string *try_name = zend_string_concat3( + ZSTR_VAL(current_scope->name), ZSTR_LEN(current_scope->name), + "\\", 1, + ZSTR_VAL(original_suffix), ZSTR_LEN(original_suffix)); + + zend_string *lc_try_name = zend_string_tolower(try_name); + + bool has_seen = zend_have_seen_symbol(lc_try_name, ZEND_SYMBOL_CLASS); + zend_string_release(lc_try_name); + + if (has_seen) { + zend_string_release(original_suffix); + return try_name; + } + zend_string_release(try_name); + + current_scope = current_scope->lexical_scope; + } + + zend_string_release(original_suffix); + return NULL; +} + static zend_string *zend_resolve_class_name(zend_string *name, uint32_t type) /* {{{ */ { char *compound; @@ -1236,6 +1292,13 @@ static zend_string *zend_resolve_class_name(zend_string *name, uint32_t type) /* } } + if (CG(active_class_entry)) { + zend_string *nested_name = zend_resolve_class_in_scope(name, CG(active_class_entry)); + if (nested_name) { + return nested_name; + } + } + /* If not fully qualified and not an alias, prepend the current namespace */ return zend_prefix_with_ns(name); } @@ -9016,10 +9079,9 @@ static void zend_compile_use_trait(zend_ast *ast) /* {{{ */ } /* }}} */ -static void zend_compile_implements(zend_ast *ast) /* {{{ */ +static void zend_compile_implements(zend_ast *ast, zend_class_entry *ce) /* {{{ */ { zend_ast_list *list = zend_ast_get_list(ast); - zend_class_entry *ce = CG(active_class_entry); zend_class_name *interface_names; uint32_t i; @@ -9077,6 +9139,42 @@ static void zend_compile_enum_backing_type(zend_class_entry *ce, zend_ast *enum_ zend_type_release(type, 0); } +static void zend_defer_class_decl(zend_ast *ast) +{ + ZEND_ASSERT(CG(active_class_entry)); + + if (CG(active_op_array)->function_name) { + zend_error_noreturn(E_COMPILE_ERROR, "Class declarations may not be declared inside functions"); + } + + if (CG(nested_class_queue) == NULL) { + ALLOC_HASHTABLE(CG(nested_class_queue)); + zend_hash_init(CG(nested_class_queue), 8, NULL, ZVAL_PTR_DTOR, 0); + } + + zend_hash_next_index_insert_ptr(CG(nested_class_queue), ast); +} + +static void zend_scan_nested_class_decl(zend_ast *ast, const zend_string *prefix) +{ + const zend_ast_list *list = zend_ast_get_list(ast); + for (int i = 0; i < list->children; i++) { + ast = list->child[i]; + if (ast->kind == ZEND_AST_CLASS) { + const zend_ast_decl *decl = (zend_ast_decl *)ast; + zend_string *name = zend_string_concat3( + ZSTR_VAL(prefix), ZSTR_LEN(prefix), + "\\", 1, + ZSTR_VAL(decl->name), ZSTR_LEN(decl->name)); + zend_string *lc_name = zend_string_tolower(name); + zend_register_seen_symbol(lc_name, ZEND_SYMBOL_CLASS); + zend_string_release(lc_name); + zend_scan_nested_class_decl(decl->child[2], name); + zend_string_release(name); + } + } +} + static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) /* {{{ */ { zend_ast_decl *decl = (zend_ast_decl *) ast; @@ -9093,10 +9191,6 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) if (EXPECTED((decl->flags & ZEND_ACC_ANON_CLASS) == 0)) { zend_string *unqualified_name = decl->name; - if (CG(active_class_entry)) { - zend_error_noreturn(E_COMPILE_ERROR, "Class declarations may not be nested"); - } - const char *type = "a class name"; if (decl->flags & ZEND_ACC_ENUM) { type = "an enum name"; @@ -9106,7 +9200,34 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) type = "a trait name"; } zend_assert_valid_class_name(unqualified_name, type); - name = zend_prefix_with_ns(unqualified_name); + + if (CG(active_class_entry)) { + name = zend_string_concat3( + ZSTR_VAL(CG(active_class_entry)->name), ZSTR_LEN(CG(active_class_entry)->name), + "\\", 1, + ZSTR_VAL(unqualified_name), ZSTR_LEN(unqualified_name)); + + /* configure the class from the modifiers */ + decl->flags |= decl->attr & ZEND_ACC_FINAL; + if (decl->attr & ZEND_ACC_ABSTRACT) { + decl->flags |= ZEND_ACC_EXPLICIT_ABSTRACT_CLASS; + } + if (decl->attr & ZEND_ACC_READONLY) { + decl->flags |= ZEND_ACC_READONLY_CLASS | ZEND_ACC_NO_DYNAMIC_PROPERTIES; + } + + int propFlags = decl->attr & ZEND_ACC_PPP_MASK; + decl->attr &= ~(ZEND_ACC_PPP_MASK | ZEND_ACC_FINAL | ZEND_ACC_READONLY | ZEND_ACC_ABSTRACT); + + ce->required_scope = propFlags & (ZEND_ACC_PRIVATE | ZEND_ACC_PROTECTED) ? CG(active_class_entry) : NULL; + ce->required_scope_absolute = propFlags & ZEND_ACC_PRIVATE ? true : false; + ce->lexical_scope = CG(active_class_entry); + } else { + name = zend_prefix_with_ns(unqualified_name); + ce->required_scope = NULL; + ce->lexical_scope = zend_resolve_namespace(FC(current_namespace)); + } + name = zend_new_interned_string(name); lcname = zend_string_tolower(name); @@ -9124,6 +9245,8 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) /* Find an anon class name that is not in use yet. */ name = NULL; lcname = NULL; + ce->required_scope = NULL; + ce->lexical_scope = CG(active_class_entry) ? CG(active_class_entry) : zend_resolve_namespace(FC(current_namespace)); do { zend_tmp_string_release(name); zend_tmp_string_release(lcname); @@ -9165,16 +9288,16 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) zend_resolve_const_class_name_reference(extends_ast, "class name"); } + if (implements_ast) { + zend_compile_implements(implements_ast, ce); + } + CG(active_class_entry) = ce; if (decl->child[3]) { zend_compile_attributes(&ce->attributes, decl->child[3], 0, ZEND_ATTRIBUTE_TARGET_CLASS, 0); } - if (implements_ast) { - zend_compile_implements(implements_ast); - } - if (ce->ce_flags & ZEND_ACC_ENUM) { if (enum_backing_type_ast != NULL) { zend_compile_enum_backing_type(ce, enum_backing_type_ast); @@ -9183,6 +9306,9 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) zend_enum_register_props(ce); } + if (ce->lexical_scope->type == ZEND_NAMESPACE_CLASS) { + zend_scan_nested_class_decl(stmt_ast, name); + } zend_compile_stmt(stmt_ast); /* Reset lineno for final opcodes and errors */ @@ -9192,8 +9318,6 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) zend_verify_abstract_class(ce); } - CG(active_class_entry) = original_ce; - if (toplevel) { ce->ce_flags |= ZEND_ACC_TOP_LEVEL; } @@ -9214,7 +9338,7 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) && !zend_compile_ignore_class(parent_ce, ce->info.user.filename)) { if (zend_try_early_bind(ce, parent_ce, lcname, NULL)) { zend_string_release(lcname); - return; + goto compile_nested_classes; } } } else if (EXPECTED(zend_hash_add_ptr(CG(class_table), lcname, ce) != NULL)) { @@ -9223,7 +9347,7 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) zend_inheritance_check_override(ce); ce->ce_flags |= ZEND_ACC_LINKED; zend_observer_class_linked_notify(ce, lcname); - return; + goto compile_nested_classes; } else { goto link_unbound; } @@ -9293,6 +9417,24 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) opline->result.opline_num = -1; } } + compile_nested_classes: + + if (CG(nested_class_queue) == NULL) { + CG(active_class_entry) = original_ce; + return; + } + + HashTable *queue = CG(nested_class_queue); + CG(nested_class_queue) = NULL; + + ZEND_HASH_FOREACH_PTR(queue, ast) { + zend_compile_class_decl(NULL, ast, true); + } ZEND_HASH_FOREACH_END(); + + CG(active_class_entry) = original_ce; + + zend_hash_destroy(queue); + FREE_HASHTABLE(queue); } /* }}} */ @@ -11583,6 +11725,10 @@ static void zend_compile_stmt(zend_ast *ast) /* {{{ */ zend_compile_use_trait(ast); break; case ZEND_AST_CLASS: + if (CG(active_class_entry)) { + zend_defer_class_decl(ast); + break; + } zend_compile_class_decl(NULL, ast, 0); break; case ZEND_AST_GROUP_USE: diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 1f55521fb72f1..7a46a2a074bc3 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -39,6 +39,7 @@ #include "zend_observer.h" #include "zend_call_stack.h" #include "zend_frameless_function.h" +#include "zend_namespaces.h" #ifdef HAVE_SYS_TIME_H #include #endif @@ -148,6 +149,8 @@ void init_executor(void) /* {{{ */ EG(function_table) = CG(function_table); EG(class_table) = CG(class_table); + EG(namespaces) = NULL; + EG(global_namespace) = NULL; EG(in_autoload) = NULL; EG(error_handling) = EH_NORMAL; @@ -496,6 +499,8 @@ void shutdown_executor(void) /* {{{ */ FREE_HASHTABLE(*EG(symtable_cache_ptr)); } + zend_destroy_namespaces(); + zend_hash_destroy(&EG(included_files)); zend_stack_destroy(&EG(user_error_handlers_error_reporting)); diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index b8422413c9c65..2e205c9288609 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -85,6 +85,7 @@ struct _zend_compiler_globals { zend_stack loop_var_stack; zend_class_entry *active_class_entry; + HashTable *nested_class_queue; zend_string *compiled_filename; diff --git a/Zend/zend_namespaces.c b/Zend/zend_namespaces.c index a7d9093357fca..85f519c7ad275 100644 --- a/Zend/zend_namespaces.c +++ b/Zend/zend_namespaces.c @@ -66,6 +66,7 @@ zend_class_entry *zend_resolve_namespace(zend_string *name) { if (EG(global_namespace) == NULL) { EG(global_namespace) = create_namespace(zend_empty_string); EG(global_namespace)->lexical_scope = NULL; + ALLOC_HASHTABLE(EG(namespaces)); zend_hash_init(EG(namespaces), 8, NULL, ZEND_CLASS_DTOR, 0); zend_hash_add_ptr(EG(namespaces), zend_empty_string, EG(global_namespace)); } @@ -93,3 +94,11 @@ zend_class_entry *zend_lookup_namespace(zend_string *name) { return ns; } + +void zend_destroy_namespaces(void) { + zend_hash_destroy(EG(namespaces)); + FREE_HASHTABLE(EG(namespaces)); + EG(namespaces) = NULL; + pefree(EG(global_namespace), 0); + EG(global_namespace) = NULL; +} diff --git a/Zend/zend_namespaces.h b/Zend/zend_namespaces.h index a1daaf839d29d..1ef876d80019c 100644 --- a/Zend/zend_namespaces.h +++ b/Zend/zend_namespaces.h @@ -24,5 +24,6 @@ ZEND_API zend_class_entry *zend_resolve_namespace(zend_string *name); ZEND_API zend_class_entry *zend_lookup_namespace(zend_string *name); +ZEND_API void zend_destroy_namespaces(void); #endif //ZEND_NAMESPACES_H From a06723e87b4157599f7d31252eae3522026438d8 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 12:21:48 +0100 Subject: [PATCH 06/26] add more tests and fix a memory leak --- Zend/tests/errmsg/errmsg_027.phpt | 2 +- Zend/zend_namespaces.c | 23 +++++++---- Zend/zend_opcode.c | 7 +++- ext/reflection/tests/bug74454.phpt | 2 +- ...ation.phpt => simple_declaration_001.phpt} | 0 .../inner_classes/simple_declaration_002.phpt | 13 +++++++ .../inner_classes/simple_declaration_003.phpt | 20 ++++++++++ .../inner_classes/simple_declaration_004.phpt | 38 +++++++++++++++++++ .../inner_classes/simple_declaration_005.phpt | 34 +++++++++++++++++ 9 files changed, 128 insertions(+), 11 deletions(-) rename tests/classes/inner_classes/{simple_declaration.phpt => simple_declaration_001.phpt} (100%) create mode 100644 tests/classes/inner_classes/simple_declaration_002.phpt create mode 100644 tests/classes/inner_classes/simple_declaration_003.phpt create mode 100644 tests/classes/inner_classes/simple_declaration_004.phpt create mode 100644 tests/classes/inner_classes/simple_declaration_005.phpt diff --git a/Zend/tests/errmsg/errmsg_027.phpt b/Zend/tests/errmsg/errmsg_027.phpt index 3d96ec27b4d64..a30e1269453f9 100644 --- a/Zend/tests/errmsg/errmsg_027.phpt +++ b/Zend/tests/errmsg/errmsg_027.phpt @@ -13,4 +13,4 @@ class test { echo "Done\n"; ?> --EXPECTF-- -Fatal error: Class declarations may not be nested in %s on line %d +Fatal error: Class declarations may not be declared inside functions in %s on line %d diff --git a/Zend/zend_namespaces.c b/Zend/zend_namespaces.c index 85f519c7ad275..f013f3ad83cba 100644 --- a/Zend/zend_namespaces.c +++ b/Zend/zend_namespaces.c @@ -39,8 +39,8 @@ static zend_class_entry *insert_namespace(const zend_string *name) { const char *pos = start; size_t len = 0; - while (pos < end) { - if (*pos == '\\') { + while (pos <= end) { + if (pos == end || *pos == '\\') { len = pos - start; zend_string *needle = zend_string_init(ZSTR_VAL(name), len, 0); @@ -51,8 +51,14 @@ static zend_class_entry *insert_namespace(const zend_string *name) { ns = create_namespace(interned_name); ns->lexical_scope = parent_ns; zend_hash_add_ptr(EG(namespaces), interned_name, ns); + + /* sometimes, opcache refuses to intern the string */ + if (interned_name == needle) { + zend_string_release(interned_name); + } + } else { + zend_string_release(needle); } - zend_string_release(needle); parent_ns = ns; } @@ -96,9 +102,10 @@ zend_class_entry *zend_lookup_namespace(zend_string *name) { } void zend_destroy_namespaces(void) { - zend_hash_destroy(EG(namespaces)); - FREE_HASHTABLE(EG(namespaces)); - EG(namespaces) = NULL; - pefree(EG(global_namespace), 0); - EG(global_namespace) = NULL; + if (EG(namespaces) != NULL) { + zend_hash_destroy(EG(namespaces)); + FREE_HASHTABLE(EG(namespaces)); + EG(namespaces) = NULL; + EG(global_namespace) = NULL; + } } diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index ce052024ae7a0..8bc272196ca9f 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -432,6 +432,7 @@ ZEND_API void destroy_zend_class(zval *zv) } break; case ZEND_INTERNAL_CLASS: + case ZEND_NAMESPACE_CLASS: if (ce->doc_comment) { zend_string_release_ex(ce->doc_comment, 1); } @@ -527,7 +528,11 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->attributes) { zend_hash_release(ce->attributes); } - free(ce); + if (ce->type == ZEND_NAMESPACE_CLASS) { + pefree(ce, 0); + } else { + free(ce); + } break; } } diff --git a/ext/reflection/tests/bug74454.phpt b/ext/reflection/tests/bug74454.phpt index 272409339c479..f1116becf6ce8 100644 --- a/ext/reflection/tests/bug74454.phpt +++ b/ext/reflection/tests/bug74454.phpt @@ -14,4 +14,4 @@ function load_file() { } ?> --EXPECT-- -ParseError: syntax error, unexpected token "if", expecting "function" +ParseError: syntax error, unexpected token "if", expecting "class" diff --git a/tests/classes/inner_classes/simple_declaration.phpt b/tests/classes/inner_classes/simple_declaration_001.phpt similarity index 100% rename from tests/classes/inner_classes/simple_declaration.phpt rename to tests/classes/inner_classes/simple_declaration_001.phpt diff --git a/tests/classes/inner_classes/simple_declaration_002.phpt b/tests/classes/inner_classes/simple_declaration_002.phpt new file mode 100644 index 0000000000000..d129f17a7b0a5 --- /dev/null +++ b/tests/classes/inner_classes/simple_declaration_002.phpt @@ -0,0 +1,13 @@ +--TEST-- +nested inside function +--FILE-- + +--EXPECTF-- +Fatal error: Class declarations may not be declared inside functions in %s on line %d diff --git a/tests/classes/inner_classes/simple_declaration_003.phpt b/tests/classes/inner_classes/simple_declaration_003.phpt new file mode 100644 index 0000000000000..1d8c59fd64ef2 --- /dev/null +++ b/tests/classes/inner_classes/simple_declaration_003.phpt @@ -0,0 +1,20 @@ +--TEST-- +basic nested classes +--FILE-- +test(); +?> +--EXPECT-- +Foo\Outer\Middle\Inner diff --git a/tests/classes/inner_classes/simple_declaration_004.phpt b/tests/classes/inner_classes/simple_declaration_004.phpt new file mode 100644 index 0000000000000..92dde65e9fcfe --- /dev/null +++ b/tests/classes/inner_classes/simple_declaration_004.phpt @@ -0,0 +1,38 @@ +--TEST-- +scope resolution access +--FILE-- + +--EXPECT-- +object(Outer\Middle)#1 (0) { +} +object(Outer\Middle)#1 (0) { +} +object(Outer2\Middle)#1 (0) { +} diff --git a/tests/classes/inner_classes/simple_declaration_005.phpt b/tests/classes/inner_classes/simple_declaration_005.phpt new file mode 100644 index 0000000000000..49427a492422e --- /dev/null +++ b/tests/classes/inner_classes/simple_declaration_005.phpt @@ -0,0 +1,34 @@ +--TEST-- +failed inheritance +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of Outer2::testSelf(): Outer2\middle must be compatible with Outer::testSelf(): Outer\middle in %s on line %d From b369064fc2684af95427e1daedb2610a7373b11d Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 14:38:22 +0100 Subject: [PATCH 07/26] add more tests --- tests/classes/inner_classes/autoload.phpt | 15 ++++++++++ tests/classes/inner_classes/enum_usage.phpt | 16 ++++++++++ tests/classes/inner_classes/errors_001.phpt | 12 ++++++++ tests/classes/inner_classes/errors_002.phpt | 15 ++++++++++ tests/classes/inner_classes/inheritance.phpt | 14 +++++++++ tests/classes/inner_classes/inner_classes.inc | 9 ++++++ .../inner_classes/interface_usage.phpt | 21 +++++++++++++ tests/classes/inner_classes/readonly.phpt | 23 ++++++++++++++ .../inner_classes/static_variables.phpt | 30 +++++++++++++++++++ .../inner_classes/trait_usage_001.phpt | 24 +++++++++++++++ 10 files changed, 179 insertions(+) create mode 100644 tests/classes/inner_classes/autoload.phpt create mode 100644 tests/classes/inner_classes/enum_usage.phpt create mode 100644 tests/classes/inner_classes/errors_001.phpt create mode 100644 tests/classes/inner_classes/errors_002.phpt create mode 100644 tests/classes/inner_classes/inheritance.phpt create mode 100644 tests/classes/inner_classes/inner_classes.inc create mode 100644 tests/classes/inner_classes/interface_usage.phpt create mode 100644 tests/classes/inner_classes/readonly.phpt create mode 100644 tests/classes/inner_classes/static_variables.phpt create mode 100644 tests/classes/inner_classes/trait_usage_001.phpt diff --git a/tests/classes/inner_classes/autoload.phpt b/tests/classes/inner_classes/autoload.phpt new file mode 100644 index 0000000000000..dc18c02c8b9de --- /dev/null +++ b/tests/classes/inner_classes/autoload.phpt @@ -0,0 +1,15 @@ +--TEST-- +ensure autoloading works +--FILE-- +x, ' ', $point->y, "\n"; +?> +--EXPECT-- +autoload(inner_classes\Point) +1 2 diff --git a/tests/classes/inner_classes/enum_usage.phpt b/tests/classes/inner_classes/enum_usage.phpt new file mode 100644 index 0000000000000..00b2c5f755a55 --- /dev/null +++ b/tests/classes/inner_classes/enum_usage.phpt @@ -0,0 +1,16 @@ +--TEST-- +usage in an enum +--FILE-- + +--EXPECT-- +object(Outer\Inner)#1 (0) { +} +bool(true) diff --git a/tests/classes/inner_classes/errors_001.phpt b/tests/classes/inner_classes/errors_001.phpt new file mode 100644 index 0000000000000..d03109ea5af51 --- /dev/null +++ b/tests/classes/inner_classes/errors_001.phpt @@ -0,0 +1,12 @@ +--TEST-- +no outer class +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught Error: Class "Outer\Inner" not found in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/errors_002.phpt b/tests/classes/inner_classes/errors_002.phpt new file mode 100644 index 0000000000000..883310c6705d0 --- /dev/null +++ b/tests/classes/inner_classes/errors_002.phpt @@ -0,0 +1,15 @@ +--TEST-- +inner class not found +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught Error: Class "Outer\Inner" not found in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/inheritance.phpt b/tests/classes/inner_classes/inheritance.phpt new file mode 100644 index 0000000000000..0aba7a65b7fa8 --- /dev/null +++ b/tests/classes/inner_classes/inheritance.phpt @@ -0,0 +1,14 @@ +--TEST-- +circular inheritance +--FILE-- + +--EXPECT-- diff --git a/tests/classes/inner_classes/inner_classes.inc b/tests/classes/inner_classes/inner_classes.inc new file mode 100644 index 0000000000000..793804cf6606e --- /dev/null +++ b/tests/classes/inner_classes/inner_classes.inc @@ -0,0 +1,9 @@ + +--EXPECT-- +object(Outer\Inner)#1 (0) { +} +bool(true) +bool(false) diff --git a/tests/classes/inner_classes/readonly.phpt b/tests/classes/inner_classes/readonly.phpt new file mode 100644 index 0000000000000..3fb85996605a3 --- /dev/null +++ b/tests/classes/inner_classes/readonly.phpt @@ -0,0 +1,23 @@ +--TEST-- +readonly should work +--FILE-- +t = 42; +var_dump($foo); +?> +--EXPECTF-- +object(Outer\Inner)#1 (1) { + ["t"]=> + int(1) +} + +Fatal error: Uncaught Error: Cannot modify readonly property Outer\Inner::$t in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/static_variables.phpt b/tests/classes/inner_classes/static_variables.phpt new file mode 100644 index 0000000000000..debe1a5198e2c --- /dev/null +++ b/tests/classes/inner_classes/static_variables.phpt @@ -0,0 +1,30 @@ +--TEST-- +::class, statics, and inner classes +--FILE-- + +--EXPECT-- +string(12) "Outer\Middle" +string(3) "foo" +int(42) +string(18) "Outer\Middle\Inner" +string(3) "foo" +int(42) diff --git a/tests/classes/inner_classes/trait_usage_001.phpt b/tests/classes/inner_classes/trait_usage_001.phpt new file mode 100644 index 0000000000000..6e618e1a09387 --- /dev/null +++ b/tests/classes/inner_classes/trait_usage_001.phpt @@ -0,0 +1,24 @@ +--TEST-- +usage inside a trait +--FILE-- + +--EXPECT-- +object(Outer\Inner)#1 (0) { +} +bool(true) +bool(false) From df7f14964f8729261a58669b9e0763ef6da811aa Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 14:42:49 +0100 Subject: [PATCH 08/26] add more tests --- .../inner_classes/access_modifiers_001.phpt | 13 +++++++++ .../inner_classes/access_modifiers_002.phpt | 13 +++++++++ .../inner_classes/access_modifiers_003.phpt | 13 +++++++++ .../inner_classes/access_modifiers_004.phpt | 13 +++++++++ .../inner_classes/access_modifiers_005.phpt | 13 +++++++++ .../inner_classes/access_modifiers_006.phpt | 13 +++++++++ .../inner_classes/access_modifiers_007.phpt | 27 +++++++++++++++++++ 7 files changed, 105 insertions(+) create mode 100644 tests/classes/inner_classes/access_modifiers_001.phpt create mode 100644 tests/classes/inner_classes/access_modifiers_002.phpt create mode 100644 tests/classes/inner_classes/access_modifiers_003.phpt create mode 100644 tests/classes/inner_classes/access_modifiers_004.phpt create mode 100644 tests/classes/inner_classes/access_modifiers_005.phpt create mode 100644 tests/classes/inner_classes/access_modifiers_006.phpt create mode 100644 tests/classes/inner_classes/access_modifiers_007.phpt diff --git a/tests/classes/inner_classes/access_modifiers_001.phpt b/tests/classes/inner_classes/access_modifiers_001.phpt new file mode 100644 index 0000000000000..ac1ce71cd3d40 --- /dev/null +++ b/tests/classes/inner_classes/access_modifiers_001.phpt @@ -0,0 +1,13 @@ +--TEST-- +multiple access modifiers +--FILE-- + +--EXPECTF-- +Fatal error: Multiple access type modifiers are not allowed in %s on line %d diff --git a/tests/classes/inner_classes/access_modifiers_002.phpt b/tests/classes/inner_classes/access_modifiers_002.phpt new file mode 100644 index 0000000000000..9c9eee89387b0 --- /dev/null +++ b/tests/classes/inner_classes/access_modifiers_002.phpt @@ -0,0 +1,13 @@ +--TEST-- +invalid inner class +--FILE-- + +--EXPECTF-- +Parse error: syntax error, unexpected identifier "int", expecting "class" in %s on line %d diff --git a/tests/classes/inner_classes/access_modifiers_003.phpt b/tests/classes/inner_classes/access_modifiers_003.phpt new file mode 100644 index 0000000000000..f6cd8bdd3eab1 --- /dev/null +++ b/tests/classes/inner_classes/access_modifiers_003.phpt @@ -0,0 +1,13 @@ +--TEST-- +static access modifiers +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use the static modifier on a nested class in %s on line %d diff --git a/tests/classes/inner_classes/access_modifiers_004.phpt b/tests/classes/inner_classes/access_modifiers_004.phpt new file mode 100644 index 0000000000000..78ba58bfcc64a --- /dev/null +++ b/tests/classes/inner_classes/access_modifiers_004.phpt @@ -0,0 +1,13 @@ +--TEST-- +public(set) inner class +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use the public(set) modifier on a nested class in %s on line %d diff --git a/tests/classes/inner_classes/access_modifiers_005.phpt b/tests/classes/inner_classes/access_modifiers_005.phpt new file mode 100644 index 0000000000000..96966f0dc384e --- /dev/null +++ b/tests/classes/inner_classes/access_modifiers_005.phpt @@ -0,0 +1,13 @@ +--TEST-- +protected(set) inner class +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use the protected(set) modifier on a nested class in %s on line %d diff --git a/tests/classes/inner_classes/access_modifiers_006.phpt b/tests/classes/inner_classes/access_modifiers_006.phpt new file mode 100644 index 0000000000000..4599ea7cfd7d9 --- /dev/null +++ b/tests/classes/inner_classes/access_modifiers_006.phpt @@ -0,0 +1,13 @@ +--TEST-- +private(set) inner class +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use the private(set) modifier on a nested class in %s on line %d diff --git a/tests/classes/inner_classes/access_modifiers_007.phpt b/tests/classes/inner_classes/access_modifiers_007.phpt new file mode 100644 index 0000000000000..88b9071b958cc --- /dev/null +++ b/tests/classes/inner_classes/access_modifiers_007.phpt @@ -0,0 +1,27 @@ +--TEST-- +abstract inner classes +--FILE-- +isAbstract()); +new Outer\Inner(); +?> +--EXPECTF-- +object(Extended)#1 (0) { +} +bool(true) + +Fatal error: Uncaught Error: Cannot instantiate abstract class Outer\Inner in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d From d3b3666e172915eee1fba0077a660e659b2c8057 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 15:13:17 +0100 Subject: [PATCH 09/26] add reflection support --- ext/reflection/php_reflection.c | 49 ++++++++++++++ ext/reflection/php_reflection.stub.php | 8 +++ ext/reflection/php_reflection_arginfo.h | 18 +++++- .../classes/inner_classes/reflection_001.phpt | 64 +++++++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 tests/classes/inner_classes/reflection_001.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 5fc4bd6b643b5..5b4c5f7a41dcc 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -4138,6 +4138,55 @@ static void add_class_vars(zend_class_entry *ce, bool statics, zval *return_valu } /* }}} */ +/* {{{ Returns whether the class is private */ +ZEND_METHOD(ReflectionClass, isPrivate) +{ + reflection_object *intern; + zend_class_entry *ce; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ce); + RETURN_BOOL(ce->required_scope && ce->required_scope_absolute); +} +/* }}} */ + +/* {{{ Returns true if the class is protected */ +ZEND_METHOD(ReflectionClass, isProtected) +{ + reflection_object *intern; + zend_class_entry *ce; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ce); + RETURN_BOOL(ce->required_scope && !ce->required_scope_absolute); +} +/* }}} */ + +/* {{{ Returns true if the class is public */ +ZEND_METHOD(ReflectionClass, isPublic) +{ + reflection_object *intern; + zend_class_entry *ce; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(ce); + RETURN_BOOL(!ce->required_scope); +} +/* }}} */ + +/* {{{ Returns whether the current class is an inner class */ +ZEND_METHOD(ReflectionClass, isInnerClass) +{ + reflection_object *intern; + zend_class_entry *ce; + ZEND_PARSE_PARAMETERS_NONE(); + + GET_REFLECTION_OBJECT_PTR(ce); + + RETURN_BOOL(ce->lexical_scope && ce->lexical_scope->type != ZEND_NAMESPACE_CLASS); +} +/* }}} */ + /* {{{ Returns an associative array containing all static property values of the class */ ZEND_METHOD(ReflectionClass, getStaticProperties) { diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index be511d7ee14cd..523c557b006ca 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -432,6 +432,14 @@ public function getNamespaceName(): string {} public function getShortName(): string {} public function getAttributes(?string $name = null, int $flags = 0): array {} + + public function isInnerClass(): bool {} + + public function isPrivate(): bool {} + + public function isPublic(): bool {} + + public function isProtected(): bool {} } class ReflectionObject extends ReflectionClass diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index d78a685dde9c9..962da4160ef52 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 3c6be99bb36965139464925a618cb0bf03affa62 */ + * Stub hash: 192b737230cefd6dd019062664e7deb49f920645 */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, modifiers, IS_LONG, 0) @@ -366,6 +366,14 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionClass_getAttributes arginfo_class_ReflectionFunctionAbstract_getAttributes +#define arginfo_class_ReflectionClass_isInnerClass arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionClass_isPrivate arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionClass_isPublic arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionClass_isProtected arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionObject___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) ZEND_END_ARG_INFO() @@ -847,6 +855,10 @@ ZEND_METHOD(ReflectionClass, inNamespace); ZEND_METHOD(ReflectionClass, getNamespaceName); ZEND_METHOD(ReflectionClass, getShortName); ZEND_METHOD(ReflectionClass, getAttributes); +ZEND_METHOD(ReflectionClass, isInnerClass); +ZEND_METHOD(ReflectionClass, isPrivate); +ZEND_METHOD(ReflectionClass, isPublic); +ZEND_METHOD(ReflectionClass, isProtected); ZEND_METHOD(ReflectionObject, __construct); ZEND_METHOD(ReflectionProperty, __construct); ZEND_METHOD(ReflectionProperty, __toString); @@ -1139,6 +1151,10 @@ static const zend_function_entry class_ReflectionClass_methods[] = { ZEND_ME(ReflectionClass, getNamespaceName, arginfo_class_ReflectionClass_getNamespaceName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, getShortName, arginfo_class_ReflectionClass_getShortName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, getAttributes, arginfo_class_ReflectionClass_getAttributes, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, isInnerClass, arginfo_class_ReflectionClass_isInnerClass, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, isPrivate, arginfo_class_ReflectionClass_isPrivate, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, isPublic, arginfo_class_ReflectionClass_isPublic, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, isProtected, arginfo_class_ReflectionClass_isProtected, ZEND_ACC_PUBLIC) ZEND_FE_END }; diff --git a/tests/classes/inner_classes/reflection_001.phpt b/tests/classes/inner_classes/reflection_001.phpt new file mode 100644 index 0000000000000..4872d1514cdcc --- /dev/null +++ b/tests/classes/inner_classes/reflection_001.phpt @@ -0,0 +1,64 @@ +--TEST-- +reflection on inner classes +--FILE-- +getName()); + var_dump($ref->getShortName()); + var_dump($ref->isInnerClass()); + var_dump($ref->isPrivate()); + var_dump($ref->isProtected()); + var_dump($ref->isPublic()); +} + +details(Outer::class); +details('n\s\Outer\Middle\Inner'); +details('n\s\Outer\PrivateMiddle'); +details('n\s\Outer\ProtectedMiddle'); + +?> +--EXPECT-- +Details for n\s\Outer +string(9) "n\s\Outer" +string(5) "Outer" +bool(false) +bool(false) +bool(false) +bool(true) +Details for n\s\Outer\Middle\Inner +string(22) "n\s\Outer\Middle\Inner" +string(5) "Inner" +bool(true) +bool(false) +bool(false) +bool(true) +Details for n\s\Outer\PrivateMiddle +string(23) "n\s\Outer\PrivateMiddle" +string(13) "PrivateMiddle" +bool(true) +bool(true) +bool(false) +bool(false) +Details for n\s\Outer\ProtectedMiddle +string(25) "n\s\Outer\ProtectedMiddle" +string(15) "ProtectedMiddle" +bool(true) +bool(false) +bool(true) +bool(false) From 0c70daeb510310d3e8e52e2f483bd537b23f9aa9 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 15:20:27 +0100 Subject: [PATCH 10/26] rewrite scopes during opcache compilation --- ext/opcache/zend_persist.c | 22 ++++++++++++++++++++++ ext/reflection/php_reflection.stub.php | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 3d45c63a98781..5f6fbecd2ac94 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -1121,6 +1121,27 @@ zend_class_entry *zend_persist_class_entry(zend_class_entry *orig_ce) return ce; } +void zend_update_ce_scopes(zend_class_entry *ce) +{ + if (ce->required_scope) { + zend_class_entry *required_scope = ce->required_scope; + + zend_class_entry *r = zend_shared_alloc_get_xlat_entry(required_scope); + if (r) { + ce->required_scope = r; + } + } + + if (ce->lexical_scope) { + zend_class_entry *lexical_scope = ce->lexical_scope; + + zend_class_entry *l = zend_shared_alloc_get_xlat_entry(lexical_scope); + if (l) { + ce->lexical_scope = l; + } + } +} + void zend_update_parent_ce(zend_class_entry *ce) { if (ce->ce_flags & ZEND_ACC_LINKED) { @@ -1294,6 +1315,7 @@ static void zend_accel_persist_class_table(HashTable *class_table) if (EXPECTED(Z_TYPE(p->val) != IS_ALIAS_PTR)) { ce = Z_PTR(p->val); zend_update_parent_ce(ce); + zend_update_ce_scopes(ce); } } ZEND_HASH_FOREACH_END(); #ifdef HAVE_JIT diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index 523c557b006ca..15b506683ed04 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -432,7 +432,7 @@ public function getNamespaceName(): string {} public function getShortName(): string {} public function getAttributes(?string $name = null, int $flags = 0): array {} - + public function isInnerClass(): bool {} public function isPrivate(): bool {} From 369af23d3d41cbd4aeacf5aec1bad76f8100f24a Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 15:35:04 +0100 Subject: [PATCH 11/26] add tests for visibility --- .../classes/inner_classes/visibility_001.phpt | 25 +++++++++++ .../classes/inner_classes/visibility_002.phpt | 30 ++++++++++++++ .../classes/inner_classes/visibility_003.phpt | 30 ++++++++++++++ .../classes/inner_classes/visibility_004.phpt | 25 +++++++++++ .../classes/inner_classes/visibility_005.phpt | 27 ++++++++++++ .../classes/inner_classes/visibility_006.phpt | 28 +++++++++++++ .../classes/inner_classes/visibility_007.phpt | 27 ++++++++++++ .../classes/inner_classes/visibility_008.phpt | 30 ++++++++++++++ .../classes/inner_classes/visibility_009.phpt | 38 +++++++++++++++++ .../classes/inner_classes/visibility_010.phpt | 41 +++++++++++++++++++ 10 files changed, 301 insertions(+) create mode 100644 tests/classes/inner_classes/visibility_001.phpt create mode 100644 tests/classes/inner_classes/visibility_002.phpt create mode 100644 tests/classes/inner_classes/visibility_003.phpt create mode 100644 tests/classes/inner_classes/visibility_004.phpt create mode 100644 tests/classes/inner_classes/visibility_005.phpt create mode 100644 tests/classes/inner_classes/visibility_006.phpt create mode 100644 tests/classes/inner_classes/visibility_007.phpt create mode 100644 tests/classes/inner_classes/visibility_008.phpt create mode 100644 tests/classes/inner_classes/visibility_009.phpt create mode 100644 tests/classes/inner_classes/visibility_010.phpt diff --git a/tests/classes/inner_classes/visibility_001.phpt b/tests/classes/inner_classes/visibility_001.phpt new file mode 100644 index 0000000000000..a1ac42e6e9736 --- /dev/null +++ b/tests/classes/inner_classes/visibility_001.phpt @@ -0,0 +1,25 @@ +--TEST-- +outer class visibility +--FILE-- +illegal = new Inner(); + } +} + +$x = new Outer(); +$x->test(); + +var_dump($x); +?> +--EXPECTF-- +Fatal error: Uncaught TypeError: Cannot assign private Outer\Inner to higher visibile property Outer::illegal in %s:%d +Stack trace: +#0 %s(%d): Outer->test() +#1 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/visibility_002.phpt b/tests/classes/inner_classes/visibility_002.phpt new file mode 100644 index 0000000000000..7444229361281 --- /dev/null +++ b/tests/classes/inner_classes/visibility_002.phpt @@ -0,0 +1,30 @@ +--TEST-- +accessing outer class private vars +--FILE-- +illegal = $this; + } + } + private Inner $illegal; + + public function test(): void { + new Inner()->test($this); + } +} + +$x = new Outer(); +$x->test(); + +var_dump($x); + +?> +--EXPECT-- +object(Outer)#1 (1) { + ["illegal":"Outer":private]=> + object(Outer\Inner)#2 (0) { + } +} diff --git a/tests/classes/inner_classes/visibility_003.phpt b/tests/classes/inner_classes/visibility_003.phpt new file mode 100644 index 0000000000000..07cbf29767ca0 --- /dev/null +++ b/tests/classes/inner_classes/visibility_003.phpt @@ -0,0 +1,30 @@ +--TEST-- +accessing outer protected vars +--FILE-- +illegal = $this; + } + } + private Inner $illegal; + + public function test(): void { + new Inner()->test($this); + } +} + +$x = new Outer(); +$x->test(); + +var_dump($x); + +?> +--EXPECT-- +object(Outer)#1 (1) { + ["illegal":"Outer":private]=> + object(Outer\Inner)#2 (0) { + } +} diff --git a/tests/classes/inner_classes/visibility_004.phpt b/tests/classes/inner_classes/visibility_004.phpt new file mode 100644 index 0000000000000..c655bbe2c8b29 --- /dev/null +++ b/tests/classes/inner_classes/visibility_004.phpt @@ -0,0 +1,25 @@ +--TEST-- +outer class visibility +--FILE-- +illegal = new Inner(); + } +} + +$x = new Outer(); +$x->test(); + +var_dump($x); +?> +--EXPECTF-- +Fatal error: Uncaught TypeError: Cannot assign protected Outer\Inner to higher visibile property Outer::illegal in %s:%d +Stack trace: +#0 %s(%d): Outer->test() +#1 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/visibility_005.phpt b/tests/classes/inner_classes/visibility_005.phpt new file mode 100644 index 0000000000000..8624c36245220 --- /dev/null +++ b/tests/classes/inner_classes/visibility_005.phpt @@ -0,0 +1,27 @@ +--TEST-- +accessing outer private methods +--FILE-- +test(); + } + } + } +} +new Outer\Middle\Inner()->test(); +?> +--EXPECT-- +Outer\Middle::test +Outer::test diff --git a/tests/classes/inner_classes/visibility_006.phpt b/tests/classes/inner_classes/visibility_006.phpt new file mode 100644 index 0000000000000..de15166bb551b --- /dev/null +++ b/tests/classes/inner_classes/visibility_006.phpt @@ -0,0 +1,28 @@ +--TEST-- +scope doesn't bypass scope +--FILE-- +test(); + } + } + } +} +new Outer\Middle\Inner()->testit(); +?> +--EXPECTF-- +Fatal error: Uncaught Error: Call to undefined method Outer\Middle\Inner::test() in %s:%d +Stack trace: +#0 %s(%d): Outer\Middle\Inner->testit() +#1 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/visibility_007.phpt b/tests/classes/inner_classes/visibility_007.phpt new file mode 100644 index 0000000000000..3da65bface64a --- /dev/null +++ b/tests/classes/inner_classes/visibility_007.phpt @@ -0,0 +1,27 @@ +--TEST-- +accessing outer protected methods +--FILE-- +test(); + } + } + } +} +new Outer\Middle\Inner()->test(); +?> +--EXPECT-- +Outer\Middle::test +Outer::test diff --git a/tests/classes/inner_classes/visibility_008.phpt b/tests/classes/inner_classes/visibility_008.phpt new file mode 100644 index 0000000000000..cbb8657fc632e --- /dev/null +++ b/tests/classes/inner_classes/visibility_008.phpt @@ -0,0 +1,30 @@ +--TEST-- +accessing sibling methods +--FILE-- +test(); + } + } +} +new Other\Inner()->test(); +?> +--EXPECT-- +Outer\Middle::test +Outer::test diff --git a/tests/classes/inner_classes/visibility_009.phpt b/tests/classes/inner_classes/visibility_009.phpt new file mode 100644 index 0000000000000..3c267d109984b --- /dev/null +++ b/tests/classes/inner_classes/visibility_009.phpt @@ -0,0 +1,38 @@ +--TEST-- +deeply nested property visibility +--FILE-- +i = 42; + var_dump($foo); + $foo = new Middle(); + $foo->i = 42; + var_dump($foo); + } + } + } +} +Outer\Middle\Inner::test(); +?> +--EXPECT-- +int(5) +int(42) +object(Outer)#1 (1) { + ["i":"Outer":private]=> + int(42) +} +object(Outer\Middle)#2 (1) { + ["i":"Outer\Middle":private]=> + int(42) +} diff --git a/tests/classes/inner_classes/visibility_010.phpt b/tests/classes/inner_classes/visibility_010.phpt new file mode 100644 index 0000000000000..b6e94ddf7d359 --- /dev/null +++ b/tests/classes/inner_classes/visibility_010.phpt @@ -0,0 +1,41 @@ +--TEST-- +constructors +--FILE-- +name = $builder->name; + $this->email = $builder->email; + } + + public readonly final class Builder { + public function __construct(public private(set) string|null $name = null, public private(set) string|null $email = null) {} + + public function withEmail(string $email): self { + return new self($this->name, $email); + } + + public function withName(string $name): self { + return new self($name, $this->email); + } + + public function build(): User { + return new User($this); + } + } +} + +$user = new User\Builder()->withName('Rob')->withEmail('rob@example.com')->build(); +var_dump($user); +?> +--EXPECT-- +object(User)#2 (2) { + ["name"]=> + string(3) "Rob" + ["email"]=> + string(15) "rob@example.com" +} From 7d134444f66fcabeeed74f73f3c362e05186eeed Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 15:35:38 +0100 Subject: [PATCH 12/26] handle protected/private lookups in outer classes --- Zend/zend_object_handlers.c | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 076d2b8bbc2dd..5e18169a8e0a2 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -381,6 +381,7 @@ static zend_always_inline uintptr_t zend_get_property_offset(zend_class_entry *c if (flags & (ZEND_ACC_CHANGED|ZEND_ACC_PRIVATE|ZEND_ACC_PROTECTED)) { zend_class_entry *scope = get_fake_or_executed_scope(); +check_lexical_scope: if (property_info->ce != scope) { if (flags & ZEND_ACC_CHANGED) { zend_property_info *p = zend_get_parent_private_property(scope, ce, member); @@ -402,6 +403,10 @@ static zend_always_inline uintptr_t zend_get_property_offset(zend_class_entry *c goto dynamic; } else { wrong: + if (scope && scope->lexical_scope && scope->lexical_scope->type != ZEND_NAMESPACE_CLASS) { + scope = scope->lexical_scope; + goto check_lexical_scope; + } /* Information was available, but we were denied access. Error out. */ if (!silent) { zend_bad_property_access(property_info, ce, member); @@ -1861,6 +1866,8 @@ ZEND_API zend_function *zend_std_get_method(zend_object **obj_ptr, zend_string * /* Check access level */ if (fbc->op_array.fn_flags & (ZEND_ACC_CHANGED|ZEND_ACC_PRIVATE|ZEND_ACC_PROTECTED)) { scope = zend_get_executed_scope(); + zend_class_entry *original_scope = scope; +check_lexical_scope: if (fbc->common.scope != scope) { if (fbc->op_array.fn_flags & ZEND_ACC_CHANGED) { @@ -1878,7 +1885,11 @@ ZEND_API zend_function *zend_std_get_method(zend_object **obj_ptr, zend_string * if (zobj->ce->__call) { fbc = zend_get_user_call_function(zobj->ce, method_name); } else { - zend_bad_method_call(fbc, method_name, scope); + if (scope && scope->lexical_scope && scope->lexical_scope->type != ZEND_NAMESPACE_CLASS) { + scope = scope->lexical_scope; + goto check_lexical_scope; + } + zend_bad_method_call(fbc, method_name, original_scope); fbc = NULL; } } @@ -1937,12 +1948,20 @@ ZEND_API zend_function *zend_std_get_static_method(zend_class_entry *ce, zend_st fbc = Z_FUNC_P(func); if (!(fbc->op_array.fn_flags & ZEND_ACC_PUBLIC)) { zend_class_entry *scope = zend_get_executed_scope(); + zend_class_entry *original_scope = scope; + +check_lexical_scope: if (UNEXPECTED(fbc->common.scope != scope)) { if (UNEXPECTED(fbc->op_array.fn_flags & ZEND_ACC_PRIVATE) || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(fbc), scope))) { zend_function *fallback_fbc = get_static_method_fallback(ce, function_name); if (!fallback_fbc) { - zend_bad_method_call(fbc, function_name, scope); + if (scope && scope->lexical_scope && scope->lexical_scope->type != ZEND_NAMESPACE_CLASS) { + scope = scope->lexical_scope; + goto check_lexical_scope; + } + + zend_bad_method_call(fbc, function_name, original_scope); } fbc = fallback_fbc; } @@ -2019,10 +2038,15 @@ ZEND_API zval *zend_std_get_static_property_with_info(zend_class_entry *ce, zend if (!(property_info->flags & ZEND_ACC_PUBLIC)) { zend_class_entry *scope = get_fake_or_executed_scope(); +check_lexical_scope: if (property_info->ce != scope) { if (UNEXPECTED(property_info->flags & ZEND_ACC_PRIVATE) || UNEXPECTED(!is_protected_compatible_scope(property_info->ce, scope))) { if (type != BP_VAR_IS) { + if (scope && scope->lexical_scope && scope->lexical_scope->type != ZEND_NAMESPACE_CLASS) { + scope = scope->lexical_scope; + goto check_lexical_scope; + } zend_bad_property_access(property_info, ce, property_name); } return NULL; @@ -2103,10 +2127,16 @@ ZEND_API zend_function *zend_std_get_constructor(zend_object *zobj) /* {{{ */ if (constructor) { if (UNEXPECTED(!(constructor->op_array.fn_flags & ZEND_ACC_PUBLIC))) { zend_class_entry *scope = get_fake_or_executed_scope(); + zend_class_entry *original_scope = scope; +check_lexical_scope: if (UNEXPECTED(constructor->common.scope != scope)) { if (UNEXPECTED(constructor->op_array.fn_flags & ZEND_ACC_PRIVATE) || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(constructor), scope))) { - zend_bad_constructor_call(constructor, scope); + if (scope && scope->lexical_scope && scope->lexical_scope->type != ZEND_NAMESPACE_CLASS) { + scope = scope->lexical_scope; + goto check_lexical_scope; + } + zend_bad_constructor_call(constructor, original_scope); zend_object_store_ctor_failed(zobj); constructor = NULL; } From 36c7c52b595323713bdd6cba48aa9a8bd3946680 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 16:23:43 +0100 Subject: [PATCH 13/26] handle private/protected types in properties --- Zend/zend_execute.c | 37 +++++++++++++++++++ .../classes/inner_classes/visibility_001.phpt | 2 +- .../classes/inner_classes/visibility_004.phpt | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 6b6af2c225f79..bcad30ddce017 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1045,8 +1045,45 @@ static zend_always_inline bool i_zend_check_property_type(const zend_property_in return zend_verify_scalar_type_hint(type_mask, property, strict, 0); } +static zend_result zend_check_type_visibility(const zend_class_entry *ce, const zend_property_info *info, uint32_t current_visibility) +{ + /* public classes are always visible */ + if (!ce->required_scope) { + return SUCCESS; + } + + /* a protected class is visible if it is a subclass of the lexical scope + * and the current visibility is protected or private */ + if (!ce->required_scope_absolute && instanceof_function(info->ce, ce->required_scope)) { + if (current_visibility & ZEND_ACC_PUBLIC) { + zend_type_error("Cannot declare protected class %s to a public property in %s::%s", ZSTR_VAL(ce->name), ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name)); + return FAILURE; + } + + return SUCCESS; + } + + /* a private class is visible if it is the same class as the lexical scope and the current visibility is private */ + if (ce->required_scope_absolute && ce->required_scope == info->ce) { + if (current_visibility < ZEND_ACC_PRIVATE) { + zend_type_error("Cannot declare private class %s to a %s property in %s::%s", ZSTR_VAL(ce->name), zend_visibility_string(current_visibility), ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name)); + return FAILURE; + } + + return SUCCESS; + } + + zend_type_error("Cannot declare %s to weaker visible property %s::%s", ZSTR_VAL(ce->name), ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name)); + return FAILURE; +} + static zend_always_inline bool i_zend_verify_property_type(const zend_property_info *info, zval *property, bool strict) { + if (Z_TYPE_P(property) == IS_OBJECT && zend_check_type_visibility(Z_OBJCE_P(property), info, info->flags)) { + zend_verify_property_type_error(info, property); + return 0; + } + if (i_zend_check_property_type(info, property, strict)) { return 1; } diff --git a/tests/classes/inner_classes/visibility_001.phpt b/tests/classes/inner_classes/visibility_001.phpt index a1ac42e6e9736..2f13eb4c93f5c 100644 --- a/tests/classes/inner_classes/visibility_001.phpt +++ b/tests/classes/inner_classes/visibility_001.phpt @@ -18,7 +18,7 @@ $x->test(); var_dump($x); ?> --EXPECTF-- -Fatal error: Uncaught TypeError: Cannot assign private Outer\Inner to higher visibile property Outer::illegal in %s:%d +Fatal error: Uncaught TypeError: Cannot declare private class Outer\Inner to a public property in Outer::illegal in %s:%d Stack trace: #0 %s(%d): Outer->test() #1 {main} diff --git a/tests/classes/inner_classes/visibility_004.phpt b/tests/classes/inner_classes/visibility_004.phpt index c655bbe2c8b29..5f6b64f3f276b 100644 --- a/tests/classes/inner_classes/visibility_004.phpt +++ b/tests/classes/inner_classes/visibility_004.phpt @@ -18,7 +18,7 @@ $x->test(); var_dump($x); ?> --EXPECTF-- -Fatal error: Uncaught TypeError: Cannot assign protected Outer\Inner to higher visibile property Outer::illegal in %s:%d +Fatal error: Uncaught TypeError: Cannot declare protected class Outer\Inner to a public property in Outer::illegal in %s:%d Stack trace: #0 %s(%d): Outer->test() #1 {main} From 3e14baf0c8e4701e012dbd6b1f50ab18706e0c7a Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 16:27:39 +0100 Subject: [PATCH 14/26] do not elide return checks for private/protected inner classes --- Zend/Optimizer/dfa_pass.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/Optimizer/dfa_pass.c b/Zend/Optimizer/dfa_pass.c index 2c3aaae065997..fe1c25bda2cc8 100644 --- a/Zend/Optimizer/dfa_pass.c +++ b/Zend/Optimizer/dfa_pass.c @@ -282,7 +282,7 @@ static inline bool can_elide_list_type( zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(*single_type)); zend_class_entry *ce = zend_optimizer_get_class_entry(script, op_array, lcname); zend_string_release(lcname); - bool result = ce && safe_instanceof(use_info->ce, ce); + bool result = ce && !ce->required_scope && safe_instanceof(use_info->ce, ce); if (result == !is_intersection) { return result; } From 977643d98577b13458ba76e22efd7c9b3769f31a Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 16:30:47 +0100 Subject: [PATCH 15/26] do not elide return checks for private/protected inner classes --- tests/classes/inner_classes/{autoload.phpt => autoload_001.phpt} | 0 tests/classes/inner_classes/autoload_002.phpt | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/classes/inner_classes/{autoload.phpt => autoload_001.phpt} (100%) create mode 100644 tests/classes/inner_classes/autoload_002.phpt diff --git a/tests/classes/inner_classes/autoload.phpt b/tests/classes/inner_classes/autoload_001.phpt similarity index 100% rename from tests/classes/inner_classes/autoload.phpt rename to tests/classes/inner_classes/autoload_001.phpt diff --git a/tests/classes/inner_classes/autoload_002.phpt b/tests/classes/inner_classes/autoload_002.phpt new file mode 100644 index 0000000000000..e69de29bb2d1d From f26f1e97cf5e88f24b4521557f7cadff0f0c0fdd Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 17:18:54 +0100 Subject: [PATCH 16/26] add and update tests --- .../tests/ReflectionClass_toString_001.phpt | 30 +++++++++++++++- tests/classes/inner_classes/autoload_002.phpt | 19 ++++++++++ .../inner_classes/return_types_001.phpt | 18 ++++++++++ .../inner_classes/return_types_002.phpt | 36 +++++++++++++++++++ .../inner_classes/return_types_003.phpt | 33 +++++++++++++++++ .../inner_classes/return_types_004.phpt | 26 ++++++++++++++ .../inner_classes/return_types_005.phpt | 26 ++++++++++++++ .../inner_classes/return_types_006.phpt | 21 +++++++++++ 8 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 tests/classes/inner_classes/return_types_001.phpt create mode 100644 tests/classes/inner_classes/return_types_002.phpt create mode 100644 tests/classes/inner_classes/return_types_003.phpt create mode 100644 tests/classes/inner_classes/return_types_004.phpt create mode 100644 tests/classes/inner_classes/return_types_005.phpt create mode 100644 tests/classes/inner_classes/return_types_006.phpt diff --git a/ext/reflection/tests/ReflectionClass_toString_001.phpt b/ext/reflection/tests/ReflectionClass_toString_001.phpt index fd5d83e917419..d505878bc1d9e 100644 --- a/ext/reflection/tests/ReflectionClass_toString_001.phpt +++ b/ext/reflection/tests/ReflectionClass_toString_001.phpt @@ -30,7 +30,7 @@ Class [ class ReflectionClass implements Stringable, Refle Property [ public string $name ] } - - Methods [64] { + - Methods [68] { Method [ private method __clone ] { - Parameters [0] { @@ -514,5 +514,33 @@ Class [ class ReflectionClass implements Stringable, Refle } - Return [ array ] } + + Method [ public method isInnerClass ] { + + - Parameters [0] { + } + - Return [ bool ] + } + + Method [ public method isPrivate ] { + + - Parameters [0] { + } + - Return [ bool ] + } + + Method [ public method isPublic ] { + + - Parameters [0] { + } + - Return [ bool ] + } + + Method [ public method isProtected ] { + + - Parameters [0] { + } + - Return [ bool ] + } } } diff --git a/tests/classes/inner_classes/autoload_002.phpt b/tests/classes/inner_classes/autoload_002.phpt index e69de29bb2d1d..8365942a86db2 100644 --- a/tests/classes/inner_classes/autoload_002.phpt +++ b/tests/classes/inner_classes/autoload_002.phpt @@ -0,0 +1,19 @@ +--TEST-- +ensure private autoloading works +--FILE-- + +--EXPECTF-- +autoload(inner_classes\Line) + +Fatal error: Uncaught Error: Cannot instantiate class inner_classes\Line from the global scope in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/return_types_001.phpt b/tests/classes/inner_classes/return_types_001.phpt new file mode 100644 index 0000000000000..4f2e5806def02 --- /dev/null +++ b/tests/classes/inner_classes/return_types_001.phpt @@ -0,0 +1,18 @@ +--TEST-- +test return types +--FILE-- + +--EXPECT-- +object(Outer\Inner)#1 (0) { +} diff --git a/tests/classes/inner_classes/return_types_002.phpt b/tests/classes/inner_classes/return_types_002.phpt new file mode 100644 index 0000000000000..160cf8f0205fd --- /dev/null +++ b/tests/classes/inner_classes/return_types_002.phpt @@ -0,0 +1,36 @@ +--TEST-- +private inner class +--FILE-- +getInner(); + } +} + +class Foo extends Outer { + public function getInner(): Outer\Inner { + var_dump(parent::getInner2()); + return new Outer\Inner(); + } +} + +$outer = new Foo(); +var_dump($outer->getInner()); +?> +--EXPECTF-- +object(Outer\Inner)#2 (0) { +} + +Fatal error: Uncaught Error: Cannot instantiate private class Outer\Inner from Foo in %s:%d +Stack trace: +#0 %s(%d): Foo->getInner() +#1 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/return_types_003.phpt b/tests/classes/inner_classes/return_types_003.phpt new file mode 100644 index 0000000000000..7b4b392167e0a --- /dev/null +++ b/tests/classes/inner_classes/return_types_003.phpt @@ -0,0 +1,33 @@ +--TEST-- +protected inner class +--FILE-- +getInner()); +var_dump(new Outer\Inner()); +?> +--EXPECTF-- +object(Outer\Inner)#2 (0) { +} + +Fatal error: Uncaught TypeError: Public method getInner cannot return protected class Outer\Inner in %s:%d +Stack trace: +#0 %s(%d): Foo->getInner() +#1 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/return_types_004.phpt b/tests/classes/inner_classes/return_types_004.phpt new file mode 100644 index 0000000000000..a5c8635031aa6 --- /dev/null +++ b/tests/classes/inner_classes/return_types_004.phpt @@ -0,0 +1,26 @@ +--TEST-- +private return types +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught TypeError: Public method getInner cannot return private class Outer\Inner in %s:%d +Stack trace: +#0 %s(%d): Outer::getInner() +#1 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/return_types_005.phpt b/tests/classes/inner_classes/return_types_005.phpt new file mode 100644 index 0000000000000..dbb4f15ddcc24 --- /dev/null +++ b/tests/classes/inner_classes/return_types_005.phpt @@ -0,0 +1,26 @@ +--TEST-- +protected return types +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught TypeError: Public method getInner cannot return protected class Outer\Inner in %s:%d +Stack trace: +#0 %s(%d): Outer::getInner() +#1 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/return_types_006.phpt b/tests/classes/inner_classes/return_types_006.phpt new file mode 100644 index 0000000000000..d530496a28b35 --- /dev/null +++ b/tests/classes/inner_classes/return_types_006.phpt @@ -0,0 +1,21 @@ +--TEST-- +returning private inner from inner method +--FILE-- +test(); } +} + +$foo = new Outer()->test(); +var_dump($foo); +?> +--EXPECT-- +object(Outer\PrivateInner)#3 (0) { +} From f28513da64f0ed96da0de512dcf2cf7d6d5f1cbf Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 17:42:00 +0100 Subject: [PATCH 17/26] handle instantiation protections --- Zend/zend_API.c | 21 +++++ Zend/zend_vm_def.h | 18 ++++ Zend/zend_vm_execute.h | 90 +++++++++++++++++++ tests/classes/inner_classes/autoload_002.phpt | 2 +- .../inner_classes/instantiation_001.phpt | 89 ++++++++++++++++++ .../inner_classes/return_types_002.phpt | 2 +- 6 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 tests/classes/inner_classes/instantiation_001.phpt diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 5bc4b4a04509f..eebcc61abc929 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1816,6 +1816,27 @@ static zend_always_inline zend_result _object_and_properties_init(zval *arg, zen return FAILURE; } + if (class_type->required_scope) { + const zend_class_entry *scope = zend_get_executed_scope(); + if (UNEXPECTED(scope == NULL)) { + zend_type_error("Cannot instantiate class %s from the global scope", ZSTR_VAL(class_type->name)); + ZVAL_NULL(arg); + Z_OBJ_P(arg) = NULL; + return FAILURE; + } + + if (class_type->required_scope_absolute) { + if (scope != class_type->required_scope && scope->lexical_scope != class_type->required_scope) { + zend_type_error("Cannot instantiate private class %s from scope %s", ZSTR_VAL(class_type->name), ZSTR_VAL(scope->name)); + ZVAL_NULL(arg); + Z_OBJ_P(arg) = NULL; + return FAILURE; + } + } else if (!instanceof_function(scope, class_type->required_scope) && !instanceof_function(scope->lexical_scope, class_type->required_scope)) { + zend_type_error("Cannot instantiate protected class %s from scope %s", ZSTR_VAL(class_type->name), ZSTR_VAL(scope->name)); + } + } + if (UNEXPECTED(!(class_type->ce_flags & ZEND_ACC_CONSTANTS_UPDATED))) { if (UNEXPECTED(zend_update_class_constants(class_type) != SUCCESS)) { ZVAL_NULL(arg); diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 6c87b81bc3bd7..b61ac653674f0 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -4427,6 +4427,24 @@ ZEND_VM_COLD_CONST_HANDLER(124, ZEND_VERIFY_RETURN_TYPE, CONST|TMP|VAR|UNUSED|CV } SAVE_OPLINE(); + + if (Z_TYPE_P(retval_ptr) == IS_OBJECT && Z_OBJCE_P(retval_ptr)->required_scope) { + if (EX(func)->common.fn_flags & ZEND_ACC_PUBLIC) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute) { + zend_type_error("Public method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } else { + zend_type_error("Public method %s cannot return protected class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } else if (EX(func)->common.fn_flags & ZEND_ACC_PROTECTED) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute && Z_OBJCE_P(retval_ptr)->required_scope != EX(func)->common.scope) { + zend_type_error("Protected method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } + } + if (UNEXPECTED(!zend_check_type_slow(&ret_info->type, retval_ptr, ref, cache_slot, 1, 0))) { zend_verify_return_error(EX(func), retval_ptr); HANDLE_EXCEPTION(); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 06b4ad6b1494a..7f93cd4f1a4fc 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -10803,6 +10803,24 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_VERIFY_RETURN_TYP } SAVE_OPLINE(); + + if (Z_TYPE_P(retval_ptr) == IS_OBJECT && Z_OBJCE_P(retval_ptr)->required_scope) { + if (EX(func)->common.fn_flags & ZEND_ACC_PUBLIC) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute) { + zend_type_error("Public method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } else { + zend_type_error("Public method %s cannot return protected class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } else if (EX(func)->common.fn_flags & ZEND_ACC_PROTECTED) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute && Z_OBJCE_P(retval_ptr)->required_scope != EX(func)->common.scope) { + zend_type_error("Protected method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } + } + if (UNEXPECTED(!zend_check_type_slow(&ret_info->type, retval_ptr, ref, cache_slot, 1, 0))) { zend_verify_return_error(EX(func), retval_ptr); HANDLE_EXCEPTION(); @@ -21540,6 +21558,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_VERIFY_RETURN_TYPE_SPEC_TMP_UN } SAVE_OPLINE(); + + if (Z_TYPE_P(retval_ptr) == IS_OBJECT && Z_OBJCE_P(retval_ptr)->required_scope) { + if (EX(func)->common.fn_flags & ZEND_ACC_PUBLIC) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute) { + zend_type_error("Public method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } else { + zend_type_error("Public method %s cannot return protected class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } else if (EX(func)->common.fn_flags & ZEND_ACC_PROTECTED) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute && Z_OBJCE_P(retval_ptr)->required_scope != EX(func)->common.scope) { + zend_type_error("Protected method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } + } + if (UNEXPECTED(!zend_check_type_slow(&ret_info->type, retval_ptr, ref, cache_slot, 1, 0))) { zend_verify_return_error(EX(func), retval_ptr); HANDLE_EXCEPTION(); @@ -30020,6 +30056,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_VERIFY_RETURN_TYPE_SPEC_VAR_UN } SAVE_OPLINE(); + + if (Z_TYPE_P(retval_ptr) == IS_OBJECT && Z_OBJCE_P(retval_ptr)->required_scope) { + if (EX(func)->common.fn_flags & ZEND_ACC_PUBLIC) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute) { + zend_type_error("Public method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } else { + zend_type_error("Public method %s cannot return protected class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } else if (EX(func)->common.fn_flags & ZEND_ACC_PROTECTED) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute && Z_OBJCE_P(retval_ptr)->required_scope != EX(func)->common.scope) { + zend_type_error("Protected method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } + } + if (UNEXPECTED(!zend_check_type_slow(&ret_info->type, retval_ptr, ref, cache_slot, 1, 0))) { zend_verify_return_error(EX(func), retval_ptr); HANDLE_EXCEPTION(); @@ -37814,6 +37868,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_VERIFY_RETURN_TYPE_SPEC_UNUSED } SAVE_OPLINE(); + + if (Z_TYPE_P(retval_ptr) == IS_OBJECT && Z_OBJCE_P(retval_ptr)->required_scope) { + if (EX(func)->common.fn_flags & ZEND_ACC_PUBLIC) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute) { + zend_type_error("Public method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } else { + zend_type_error("Public method %s cannot return protected class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } else if (EX(func)->common.fn_flags & ZEND_ACC_PROTECTED) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute && Z_OBJCE_P(retval_ptr)->required_scope != EX(func)->common.scope) { + zend_type_error("Protected method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } + } + if (UNEXPECTED(!zend_check_type_slow(&ret_info->type, retval_ptr, ref, cache_slot, 1, 0))) { zend_verify_return_error(EX(func), retval_ptr); HANDLE_EXCEPTION(); @@ -50615,6 +50687,24 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_VERIFY_RETURN_TYPE_SPEC_CV_UNU } SAVE_OPLINE(); + + if (Z_TYPE_P(retval_ptr) == IS_OBJECT && Z_OBJCE_P(retval_ptr)->required_scope) { + if (EX(func)->common.fn_flags & ZEND_ACC_PUBLIC) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute) { + zend_type_error("Public method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } else { + zend_type_error("Public method %s cannot return protected class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } else if (EX(func)->common.fn_flags & ZEND_ACC_PROTECTED) { + if (Z_OBJCE_P(retval_ptr)->required_scope_absolute && Z_OBJCE_P(retval_ptr)->required_scope != EX(func)->common.scope) { + zend_type_error("Protected method %s cannot return private class %s", ZSTR_VAL(EX(func)->common.function_name), ZSTR_VAL(Z_OBJCE_P(retval_ptr)->name)); + HANDLE_EXCEPTION(); + } + } + } + if (UNEXPECTED(!zend_check_type_slow(&ret_info->type, retval_ptr, ref, cache_slot, 1, 0))) { zend_verify_return_error(EX(func), retval_ptr); HANDLE_EXCEPTION(); diff --git a/tests/classes/inner_classes/autoload_002.phpt b/tests/classes/inner_classes/autoload_002.phpt index 8365942a86db2..505ab8bfea8ae 100644 --- a/tests/classes/inner_classes/autoload_002.phpt +++ b/tests/classes/inner_classes/autoload_002.phpt @@ -13,7 +13,7 @@ var_dump($line); --EXPECTF-- autoload(inner_classes\Line) -Fatal error: Uncaught Error: Cannot instantiate class inner_classes\Line from the global scope in %s:%d +Fatal error: Uncaught TypeError: Cannot instantiate class inner_classes\Line from the global scope in %s:%d Stack trace: #0 {main} thrown in %s on line %d diff --git a/tests/classes/inner_classes/instantiation_001.phpt b/tests/classes/inner_classes/instantiation_001.phpt new file mode 100644 index 0000000000000..32d860414cbba --- /dev/null +++ b/tests/classes/inner_classes/instantiation_001.phpt @@ -0,0 +1,89 @@ +--TEST-- +instantiation from various scopes +--FILE-- +getMessage()}\n"; +} + +try { + var_dump(new Outer\ProtectedInner()); +} catch (Throwable $e) { + echo "Failed to instantiate Outer\ProtectedInner: {$e->getMessage()}\n"; +} + +try { + var_dump(new Outer\PublicInner()); +} catch (Throwable $e) { + echo "Failed to instantiate Outer\PublicInner: {$e->getMessage()}\n"; +} + +class Other { + public function testPrivate() { + var_dump(new Outer\PrivateInner()); + } + public function testProtected() { + var_dump(new Outer\ProtectedInner()); + } + public function testPublic() { + var_dump(new Outer\PublicInner()); + } +} + +$other = new Other(); +foreach (['Private', 'Protected', 'Public'] as $type) { + try { + $other->{"test$type"}(); + } catch(Throwable $e) { + echo "Failed to instantiate Outer\\$type: {$e->getMessage()}\n"; + } +} + +class Child extends Outer { + public function testPrivate() { + var_dump(new Outer\PrivateInner()); + } + public function testProtected() { + var_dump(new Outer\ProtectedInner()); + } + public function testPublic() { + var_dump(new Outer\PublicInner()); + } +} + +$other = new Child(); +foreach (['Private', 'Protected', 'Public'] as $type) { + try { + $other->{"test$type"}(); + } catch(Throwable $e) { + echo "Failed to instantiate Outer\\$type: {$e->getMessage()}\n"; + } +} + +?> +--EXPECT-- +Failed to instantiate Outer\PrivateInner: Cannot instantiate class Outer\PrivateInner from the global scope +Failed to instantiate Outer\ProtectedInner: Cannot instantiate class Outer\ProtectedInner from the global scope +object(Outer\PublicInner)#1 (0) { +} +Failed to instantiate Outer\Private: Cannot instantiate private class Outer\PrivateInner from scope Other +Failed to instantiate Outer\Protected: Cannot instantiate protected class Outer\ProtectedInner from scope Other +object(Outer\PublicInner)#3 (0) { +} +Failed to instantiate Outer\Private: Cannot instantiate private class Outer\PrivateInner from scope Child +object(Outer\ProtectedInner)#2 (0) { +} +object(Outer\PublicInner)#2 (0) { +} diff --git a/tests/classes/inner_classes/return_types_002.phpt b/tests/classes/inner_classes/return_types_002.phpt index 160cf8f0205fd..cbd043a815d1f 100644 --- a/tests/classes/inner_classes/return_types_002.phpt +++ b/tests/classes/inner_classes/return_types_002.phpt @@ -29,7 +29,7 @@ var_dump($outer->getInner()); object(Outer\Inner)#2 (0) { } -Fatal error: Uncaught Error: Cannot instantiate private class Outer\Inner from Foo in %s:%d +Fatal error: Uncaught TypeError: Cannot instantiate private class Outer\Inner from scope Foo in %s:%d Stack trace: #0 %s(%d): Foo->getInner() #1 {main} From 8e5f4d9744ac09d668f09c7a478e47db7523ac05 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 22:04:11 +0100 Subject: [PATCH 18/26] start keeping track of own nested classes --- Zend/zend.h | 2 ++ Zend/zend_compile.c | 5 +++++ Zend/zend_opcode.c | 2 ++ tests/classes/inner_classes/inheritance.phpt | 6 ++++++ .../classes/inner_classes/resolution_001.phpt | 15 +++++++++++++ .../classes/inner_classes/resolution_002.phpt | 21 +++++++++++++++++++ .../inner_classes/simple_declaration_005.phpt | 2 +- 7 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/classes/inner_classes/resolution_001.phpt create mode 100644 tests/classes/inner_classes/resolution_002.phpt diff --git a/Zend/zend.h b/Zend/zend.h index bb05c40a2ffe1..204fc814ce91b 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -210,6 +210,7 @@ struct _zend_class_entry { uint32_t num_traits; uint32_t num_hooked_props; uint32_t num_hooked_prop_variance_checks; + uint32_t num_nested_classes; /* class_entry or string(s) depending on ZEND_ACC_LINKED */ union { @@ -218,6 +219,7 @@ struct _zend_class_entry { }; zend_class_name *trait_names; + zend_class_entry **nested_classes; zend_trait_alias **trait_aliases; zend_trait_precedence **trait_precedences; HashTable *attributes; diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 8b32d9d88b35e..5b8b1950fdbbe 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2201,6 +2201,8 @@ ZEND_API void zend_initialize_class_data(zend_class_entry *ce, bool nullify_hand ce->trait_names = NULL; ce->trait_aliases = NULL; ce->trait_precedences = NULL; + ce->num_nested_classes = 0; + ce->nested_classes = NULL; ce->serialize = NULL; ce->unserialize = NULL; if (ce->type == ZEND_INTERNAL_CLASS) { @@ -9222,6 +9224,9 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) ce->required_scope = propFlags & (ZEND_ACC_PRIVATE | ZEND_ACC_PROTECTED) ? CG(active_class_entry) : NULL; ce->required_scope_absolute = propFlags & ZEND_ACC_PRIVATE ? true : false; ce->lexical_scope = CG(active_class_entry); + ce->lexical_scope->nested_classes = + erealloc(ce->lexical_scope->nested_classes, sizeof(zend_class_entry *) * (ce->lexical_scope->num_nested_classes + 1)); + ce->lexical_scope->nested_classes[ce->lexical_scope->num_nested_classes++] = ce; } else { name = zend_prefix_with_ns(unqualified_name); ce->required_scope = NULL; diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 8bc272196ca9f..fc2253d7092a9 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -366,6 +366,8 @@ ZEND_API void destroy_zend_class(zval *zv) } } + efree(ce->nested_classes); + if (ce->default_properties_table) { zval *p = ce->default_properties_table; zval *end = p + ce->default_properties_count; diff --git a/tests/classes/inner_classes/inheritance.phpt b/tests/classes/inner_classes/inheritance.phpt index 0aba7a65b7fa8..b104070169a0a 100644 --- a/tests/classes/inner_classes/inheritance.phpt +++ b/tests/classes/inner_classes/inheritance.phpt @@ -1,5 +1,11 @@ --TEST-- circular inheritance +--SKIPIF-- + --FILE-- +--EXPECT-- +1 diff --git a/tests/classes/inner_classes/resolution_002.phpt b/tests/classes/inner_classes/resolution_002.phpt new file mode 100644 index 0000000000000..144dd11160167 --- /dev/null +++ b/tests/classes/inner_classes/resolution_002.phpt @@ -0,0 +1,21 @@ +--TEST-- +shadowing +--FILE-- + +--EXPECT-- +Foo\Bar\Outer\Inner: 1 +Foo\Bar\Outer\Middle: 1 diff --git a/tests/classes/inner_classes/simple_declaration_005.phpt b/tests/classes/inner_classes/simple_declaration_005.phpt index 49427a492422e..0e28a13d44b53 100644 --- a/tests/classes/inner_classes/simple_declaration_005.phpt +++ b/tests/classes/inner_classes/simple_declaration_005.phpt @@ -31,4 +31,4 @@ var_dump(Outer2::testSelf()); ?> --EXPECTF-- -Fatal error: Declaration of Outer2::testSelf(): Outer2\middle must be compatible with Outer::testSelf(): Outer\middle in %s on line %d +Fatal error: Declaration of Outer2::testSelf(): Outer2\Middle must be compatible with Outer::testSelf(): Outer\Middle in %s on line %d From b9ff2ec631815467c27271e0bc53a5d5130f72d2 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 22:05:11 +0100 Subject: [PATCH 19/26] simplify resolution logic --- Zend/zend_compile.c | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 5b8b1950fdbbe..e8441a0381241 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1180,41 +1180,15 @@ static zend_string *zend_resolve_const_name(zend_string *name, uint32_t type, bo name, type, is_fully_qualified, 1, FC(imports_const)); } -static zend_string *get_namespace_from_scope(const zend_class_entry *scope) -{ - ZEND_ASSERT(scope != NULL); - while (scope && scope->lexical_scope && scope->type != ZEND_NAMESPACE_CLASS) { - scope = scope->lexical_scope; - } - return zend_string_copy(scope->name); -} - -static zend_string *get_scoped_name(zend_string *ns, zend_string *name) -{ - name = zend_string_tolower(name); - if (ns && ZSTR_LEN(ns) && ZSTR_LEN(name) > ZSTR_LEN(ns) + 1 && - memcmp(ZSTR_VAL(name), ZSTR_VAL(ns), ZSTR_LEN(ns)) == 0 && - ZSTR_VAL(name)[ZSTR_LEN(ns)] == '\\') { - zend_string *ret = zend_string_init(ZSTR_VAL(name) + ZSTR_LEN(ns) + 1, ZSTR_LEN(name) - ZSTR_LEN(ns) - 1, 0); - zend_string_release(name); - return ret; - } - return name; -} - zend_string *zend_resolve_class_in_scope(zend_string *name, const zend_class_entry *scope) { - zend_string *ns_name = get_namespace_from_scope(scope); - zend_string *original_suffix = get_scoped_name(ns_name, name); - zend_string_release(ns_name); - const zend_class_entry *current_scope = scope; while (current_scope && current_scope->type != ZEND_NAMESPACE_CLASS) { zend_string *try_name = zend_string_concat3( ZSTR_VAL(current_scope->name), ZSTR_LEN(current_scope->name), "\\", 1, - ZSTR_VAL(original_suffix), ZSTR_LEN(original_suffix)); + ZSTR_VAL(name), ZSTR_LEN(name)); zend_string *lc_try_name = zend_string_tolower(try_name); @@ -1222,7 +1196,6 @@ zend_string *zend_resolve_class_in_scope(zend_string *name, const zend_class_ent zend_string_release(lc_try_name); if (has_seen) { - zend_string_release(original_suffix); return try_name; } zend_string_release(try_name); @@ -1230,7 +1203,6 @@ zend_string *zend_resolve_class_in_scope(zend_string *name, const zend_class_ent current_scope = current_scope->lexical_scope; } - zend_string_release(original_suffix); return NULL; } From fd69299e8ba79ff35779ddc99a79997687a4f7e8 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 26 Mar 2025 22:05:23 +0100 Subject: [PATCH 20/26] keep cased namespace names --- Zend/zend.h | 2 -- Zend/zend_compile.c | 5 ----- Zend/zend_namespaces.c | 22 ++++++++-------------- Zend/zend_opcode.c | 2 -- 4 files changed, 8 insertions(+), 23 deletions(-) diff --git a/Zend/zend.h b/Zend/zend.h index 204fc814ce91b..bb05c40a2ffe1 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -210,7 +210,6 @@ struct _zend_class_entry { uint32_t num_traits; uint32_t num_hooked_props; uint32_t num_hooked_prop_variance_checks; - uint32_t num_nested_classes; /* class_entry or string(s) depending on ZEND_ACC_LINKED */ union { @@ -219,7 +218,6 @@ struct _zend_class_entry { }; zend_class_name *trait_names; - zend_class_entry **nested_classes; zend_trait_alias **trait_aliases; zend_trait_precedence **trait_precedences; HashTable *attributes; diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index e8441a0381241..146972ade00ed 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2173,8 +2173,6 @@ ZEND_API void zend_initialize_class_data(zend_class_entry *ce, bool nullify_hand ce->trait_names = NULL; ce->trait_aliases = NULL; ce->trait_precedences = NULL; - ce->num_nested_classes = 0; - ce->nested_classes = NULL; ce->serialize = NULL; ce->unserialize = NULL; if (ce->type == ZEND_INTERNAL_CLASS) { @@ -9196,9 +9194,6 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) ce->required_scope = propFlags & (ZEND_ACC_PRIVATE | ZEND_ACC_PROTECTED) ? CG(active_class_entry) : NULL; ce->required_scope_absolute = propFlags & ZEND_ACC_PRIVATE ? true : false; ce->lexical_scope = CG(active_class_entry); - ce->lexical_scope->nested_classes = - erealloc(ce->lexical_scope->nested_classes, sizeof(zend_class_entry *) * (ce->lexical_scope->num_nested_classes + 1)); - ce->lexical_scope->nested_classes[ce->lexical_scope->num_nested_classes++] = ce; } else { name = zend_prefix_with_ns(unqualified_name); ce->required_scope = NULL; diff --git a/Zend/zend_namespaces.c b/Zend/zend_namespaces.c index f013f3ad83cba..a85bb197d5336 100644 --- a/Zend/zend_namespaces.c +++ b/Zend/zend_namespaces.c @@ -26,12 +26,12 @@ zend_class_entry *create_namespace(zend_string *name) { zend_initialize_class_data(ns, 1); ns->type = ZEND_NAMESPACE_CLASS; ns->ce_flags |= ZEND_ACC_UNINSTANTIABLE; - ns->name = zend_string_copy(name); + ns->name = name; return ns; } -static zend_class_entry *insert_namespace(const zend_string *name) { +static zend_class_entry *insert_namespace(const zend_string *name, zend_string *lc_name) { zend_class_entry *parent_ns = EG(global_namespace); zend_class_entry *ns = parent_ns; const char *start = ZSTR_VAL(name); @@ -42,23 +42,17 @@ static zend_class_entry *insert_namespace(const zend_string *name) { while (pos <= end) { if (pos == end || *pos == '\\') { len = pos - start; - zend_string *needle = zend_string_init(ZSTR_VAL(name), len, 0); + zend_string *needle = zend_string_init(ZSTR_VAL(lc_name), len, 0); ns = zend_hash_find_ptr(EG(namespaces), needle); if (!ns) { - zend_string *interned_name = zend_new_interned_string(needle); - ns = create_namespace(interned_name); + zend_string *full_name = zend_string_init_interned(ZSTR_VAL(name), len, 1); + ns = create_namespace(full_name); ns->lexical_scope = parent_ns; - zend_hash_add_ptr(EG(namespaces), interned_name, ns); - - /* sometimes, opcache refuses to intern the string */ - if (interned_name == needle) { - zend_string_release(interned_name); - } - } else { - zend_string_release(needle); + zend_hash_add_ptr(EG(namespaces), needle, ns); } + zend_string_release(needle); parent_ns = ns; } @@ -85,7 +79,7 @@ zend_class_entry *zend_resolve_namespace(zend_string *name) { zend_class_entry *ns = zend_hash_find_ptr(EG(namespaces), lc_name); if (!ns) { - ns = insert_namespace(lc_name); + ns = insert_namespace(name, lc_name); } zend_string_release(lc_name); diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index fc2253d7092a9..8bc272196ca9f 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -366,8 +366,6 @@ ZEND_API void destroy_zend_class(zval *zv) } } - efree(ce->nested_classes); - if (ce->default_properties_table) { zval *p = ce->default_properties_table; zval *end = p + ce->default_properties_count; From 7e754b7bdf1dfb9a3650e9864d1bb426f10df56a Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Thu, 27 Mar 2025 19:00:45 +0100 Subject: [PATCH 21/26] add alias tests --- tests/classes/inner_classes/aliases.phpt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/classes/inner_classes/aliases.phpt diff --git a/tests/classes/inner_classes/aliases.phpt b/tests/classes/inner_classes/aliases.phpt new file mode 100644 index 0000000000000..3b1fe64e403a8 --- /dev/null +++ b/tests/classes/inner_classes/aliases.phpt @@ -0,0 +1,18 @@ +--TEST-- +aliases cannot escape +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught TypeError: Cannot instantiate class Outer\Inner from the global scope in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d From f7161a90081458ea4d57162f3fa56b53afd99ae2 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 30 Mar 2025 16:02:27 +0200 Subject: [PATCH 22/26] add more tests and fix a small visibility issue --- Zend/zend_API.c | 15 +++++--- .../classes/inner_classes/visibility_011.phpt | 34 +++++++++++++++++++ .../classes/inner_classes/visibility_012.phpt | 21 ++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 tests/classes/inner_classes/visibility_011.phpt create mode 100644 tests/classes/inner_classes/visibility_012.phpt diff --git a/Zend/zend_API.c b/Zend/zend_API.c index eebcc61abc929..dd59e695c3332 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1816,8 +1816,10 @@ static zend_always_inline zend_result _object_and_properties_init(zval *arg, zen return FAILURE; } - if (class_type->required_scope) { - const zend_class_entry *scope = zend_get_executed_scope(); + const zend_class_entry *check_class = class_type; + const zend_class_entry *scope = zend_get_executed_scope(); +check_lexical_scope: + if (check_class->required_scope) { if (UNEXPECTED(scope == NULL)) { zend_type_error("Cannot instantiate class %s from the global scope", ZSTR_VAL(class_type->name)); ZVAL_NULL(arg); @@ -1825,8 +1827,8 @@ static zend_always_inline zend_result _object_and_properties_init(zval *arg, zen return FAILURE; } - if (class_type->required_scope_absolute) { - if (scope != class_type->required_scope && scope->lexical_scope != class_type->required_scope) { + if (check_class->required_scope_absolute) { + if (scope != check_class->required_scope && scope->lexical_scope != check_class->required_scope) { zend_type_error("Cannot instantiate private class %s from scope %s", ZSTR_VAL(class_type->name), ZSTR_VAL(scope->name)); ZVAL_NULL(arg); Z_OBJ_P(arg) = NULL; @@ -1837,6 +1839,11 @@ static zend_always_inline zend_result _object_and_properties_init(zval *arg, zen } } + if (check_class != scope && check_class->lexical_scope && check_class->lexical_scope->type != ZEND_NAMESPACE_CLASS) { + check_class = check_class->lexical_scope; + goto check_lexical_scope; + } + if (UNEXPECTED(!(class_type->ce_flags & ZEND_ACC_CONSTANTS_UPDATED))) { if (UNEXPECTED(zend_update_class_constants(class_type) != SUCCESS)) { ZVAL_NULL(arg); diff --git a/tests/classes/inner_classes/visibility_011.phpt b/tests/classes/inner_classes/visibility_011.phpt new file mode 100644 index 0000000000000..36fbd3dc37c23 --- /dev/null +++ b/tests/classes/inner_classes/visibility_011.phpt @@ -0,0 +1,34 @@ +--TEST-- +returning private class via interface +--FILE-- +foo($return = Middle\Inner::foo()); + return $return; + } +} + +var_dump(new Outer()->test()); +var_dump(new Outer\Middle\Inner()); +?> +--EXPECTF-- +object(Outer\Middle\Inner)#2 (0) { +} + +Fatal error: Uncaught TypeError: Cannot instantiate class Outer\Middle\Inner from the global scope in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/tests/classes/inner_classes/visibility_012.phpt b/tests/classes/inner_classes/visibility_012.phpt new file mode 100644 index 0000000000000..2e2ca791575c7 --- /dev/null +++ b/tests/classes/inner_classes/visibility_012.phpt @@ -0,0 +1,21 @@ +--TEST-- +instantiate nested private classes +--FILE-- +inner = new Middle\Inner(); + } +} + +$x = new Outer(); +echo "success\n"; +?> +--EXPECT-- +success From 9117b02fa1fd4db10746e9d64d555b57532a68b9 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 30 Mar 2025 16:36:51 +0200 Subject: [PATCH 23/26] validate scope is set --- Zend/zend_API.c | 3 ++- Zend/zend_namespaces.c | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index dd59e695c3332..c1cbad46c0237 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1839,7 +1839,8 @@ static zend_always_inline zend_result _object_and_properties_init(zval *arg, zen } } - if (check_class != scope && check_class->lexical_scope && check_class->lexical_scope->type != ZEND_NAMESPACE_CLASS) { + // preloading may have changed the class type from ZEND_NAMESPACE_CLASS, so we need to check for user/internal class + if (check_class != scope && check_class->lexical_scope && (check_class->lexical_scope->type != ZEND_USER_CLASS && check_class->lexical_scope->type != ZEND_INTERNAL_CLASS)) { check_class = check_class->lexical_scope; goto check_lexical_scope; } diff --git a/Zend/zend_namespaces.c b/Zend/zend_namespaces.c index a85bb197d5336..771dd5ffdd2c4 100644 --- a/Zend/zend_namespaces.c +++ b/Zend/zend_namespaces.c @@ -27,6 +27,8 @@ zend_class_entry *create_namespace(zend_string *name) { ns->type = ZEND_NAMESPACE_CLASS; ns->ce_flags |= ZEND_ACC_UNINSTANTIABLE; ns->name = name; + ns->required_scope = NULL; + ns->lexical_scope = NULL; return ns; } From 264074876b38da17869ea0928817d0692ad16e3c Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 30 Mar 2025 22:47:40 +0200 Subject: [PATCH 24/26] allow enums and traits --- Zend/zend_language_parser.y | 2 ++ tests/classes/inner_classes/enum_001.phpt | 16 ++++++++++++++++ tests/classes/inner_classes/trait_001.phpt | 20 ++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 tests/classes/inner_classes/enum_001.phpt create mode 100644 tests/classes/inner_classes/trait_001.phpt diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 3c8785477971b..fac5bcca731f2 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -954,6 +954,8 @@ class_statement_list: nested_class_statement: T_CLASS T_STRING { $$ = CG(zend_lineno); } extends_from implements_list backup_doc_comment '{' class_statement_list '}' { $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $3, $6, zend_ast_get_str($2), $4, $5, $8, NULL, NULL); } + | trait_declaration_statement { $$ = $1; } + | enum_declaration_statement { $$ = $1; } ; attributed_class_statement: diff --git a/tests/classes/inner_classes/enum_001.phpt b/tests/classes/inner_classes/enum_001.phpt new file mode 100644 index 0000000000000..575e57b4b67bd --- /dev/null +++ b/tests/classes/inner_classes/enum_001.phpt @@ -0,0 +1,16 @@ +--TEST-- +nested enum +--FILE-- +name . "\n"; +?> +--EXPECT-- +Foo diff --git a/tests/classes/inner_classes/trait_001.phpt b/tests/classes/inner_classes/trait_001.phpt new file mode 100644 index 0000000000000..d923c4cc04b2b --- /dev/null +++ b/tests/classes/inner_classes/trait_001.phpt @@ -0,0 +1,20 @@ +--TEST-- +nested traits +--FILE-- +foo(); +?> +--EXPECT-- +foo From 10b2f6301c29d64d4d26c3e456a945f43d5554c0 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 30 Mar 2025 23:32:42 +0200 Subject: [PATCH 25/26] only allow enums and fix test --- Zend/zend_API.c | 2 +- Zend/zend_compile.c | 5 +++ Zend/zend_execute.c | 8 +++- Zend/zend_language_parser.y | 1 - .../inner_classes/access_modifiers_002.phpt | 2 +- tests/classes/inner_classes/enum_002.phpt | 45 +++++++++++++++++++ tests/classes/inner_classes/trait_001.phpt | 20 --------- .../inner_classes/trait_usage_001.phpt | 7 +-- 8 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 tests/classes/inner_classes/enum_002.phpt delete mode 100644 tests/classes/inner_classes/trait_001.phpt diff --git a/Zend/zend_API.c b/Zend/zend_API.c index c1cbad46c0237..0e7c7a85d4c79 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1840,7 +1840,7 @@ static zend_always_inline zend_result _object_and_properties_init(zval *arg, zen } // preloading may have changed the class type from ZEND_NAMESPACE_CLASS, so we need to check for user/internal class - if (check_class != scope && check_class->lexical_scope && (check_class->lexical_scope->type != ZEND_USER_CLASS && check_class->lexical_scope->type != ZEND_INTERNAL_CLASS)) { + if (check_class != scope && check_class->lexical_scope && (check_class->lexical_scope->type == ZEND_USER_CLASS || check_class->lexical_scope->type == ZEND_INTERNAL_CLASS)) { check_class = check_class->lexical_scope; goto check_lexical_scope; } diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 146972ade00ed..4185f9ec0e47a 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9179,6 +9179,11 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) "\\", 1, ZSTR_VAL(unqualified_name), ZSTR_LEN(unqualified_name)); + if (CG(active_class_entry)->ce_flags & ZEND_ACC_TRAIT) { + zend_error_noreturn(E_COMPILE_ERROR, "Cannot declare class %s inside a trait", + ZSTR_VAL(name)); + } + /* configure the class from the modifiers */ decl->flags |= decl->attr & ZEND_ACC_FINAL; if (decl->attr & ZEND_ACC_ABSTRACT) { diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index bcad30ddce017..5221a9acae414 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1047,6 +1047,7 @@ static zend_always_inline bool i_zend_check_property_type(const zend_property_in static zend_result zend_check_type_visibility(const zend_class_entry *ce, const zend_property_info *info, uint32_t current_visibility) { +check_lexical_scope: /* public classes are always visible */ if (!ce->required_scope) { return SUCCESS; @@ -1065,7 +1066,7 @@ static zend_result zend_check_type_visibility(const zend_class_entry *ce, const /* a private class is visible if it is the same class as the lexical scope and the current visibility is private */ if (ce->required_scope_absolute && ce->required_scope == info->ce) { - if (current_visibility < ZEND_ACC_PRIVATE) { + if ((current_visibility & ZEND_ACC_PPP_MASK) < ZEND_ACC_PRIVATE) { zend_type_error("Cannot declare private class %s to a %s property in %s::%s", ZSTR_VAL(ce->name), zend_visibility_string(current_visibility), ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name)); return FAILURE; } @@ -1073,6 +1074,11 @@ static zend_result zend_check_type_visibility(const zend_class_entry *ce, const return SUCCESS; } + if (ce->lexical_scope != info->ce && ce->lexical_scope && (ce->lexical_scope->type == ZEND_USER_CLASS || ce->lexical_scope->type == ZEND_INTERNAL_CLASS)) { + ce = ce->lexical_scope; + goto check_lexical_scope; + } + zend_type_error("Cannot declare %s to weaker visible property %s::%s", ZSTR_VAL(ce->name), ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name)); return FAILURE; } diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index fac5bcca731f2..6afde6d08e832 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -954,7 +954,6 @@ class_statement_list: nested_class_statement: T_CLASS T_STRING { $$ = CG(zend_lineno); } extends_from implements_list backup_doc_comment '{' class_statement_list '}' { $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $3, $6, zend_ast_get_str($2), $4, $5, $8, NULL, NULL); } - | trait_declaration_statement { $$ = $1; } | enum_declaration_statement { $$ = $1; } ; diff --git a/tests/classes/inner_classes/access_modifiers_002.phpt b/tests/classes/inner_classes/access_modifiers_002.phpt index 9c9eee89387b0..e12126a33a43b 100644 --- a/tests/classes/inner_classes/access_modifiers_002.phpt +++ b/tests/classes/inner_classes/access_modifiers_002.phpt @@ -10,4 +10,4 @@ class Outer { ?> --EXPECTF-- -Parse error: syntax error, unexpected identifier "int", expecting "class" in %s on line %d +Parse error: syntax error, unexpected identifier "int", expecting "class" or "enum" in %s on line %d diff --git a/tests/classes/inner_classes/enum_002.phpt b/tests/classes/inner_classes/enum_002.phpt new file mode 100644 index 0000000000000..f87becff22ded --- /dev/null +++ b/tests/classes/inner_classes/enum_002.phpt @@ -0,0 +1,45 @@ +--TEST-- +nested enums +--FILE-- +status, $this->processing]) { + [Status::Active, Processing::Pending] => "Active and Pending", + [Status::Active, Processing::Completed] => "Active and Completed", + [Status::Inactive, Processing::Pending] => "Inactive and Pending", + [Status::Inactive, Processing::Completed] => throw new LogicException('should not happen'), + }; + } + } + + public function createMessage(): Message { + return new Message($this); + } +} + +var_dump(Status::Active->createMessage()); +echo Status::Inactive->createMessage()->getMessage() . "\n"; +?> +--EXPECT-- +object(Status\Message)#3 (2) { + ["processing":"Status\Message":private]=> + enum(Status\Processing::Pending) + ["status":"Status\Message":private]=> + enum(Status::Active) +} +Inactive and Pending diff --git a/tests/classes/inner_classes/trait_001.phpt b/tests/classes/inner_classes/trait_001.phpt deleted file mode 100644 index d923c4cc04b2b..0000000000000 --- a/tests/classes/inner_classes/trait_001.phpt +++ /dev/null @@ -1,20 +0,0 @@ ---TEST-- -nested traits ---FILE-- -foo(); -?> ---EXPECT-- -foo diff --git a/tests/classes/inner_classes/trait_usage_001.phpt b/tests/classes/inner_classes/trait_usage_001.phpt index 6e618e1a09387..09cca1086e0bc 100644 --- a/tests/classes/inner_classes/trait_usage_001.phpt +++ b/tests/classes/inner_classes/trait_usage_001.phpt @@ -17,8 +17,5 @@ var_dump(class_exists(Outer\Inner::class)); var_dump(class_exists(Foo\Inner::class)); ?> ---EXPECT-- -object(Outer\Inner)#1 (0) { -} -bool(true) -bool(false) +--EXPECTF-- +Fatal error: Cannot declare class Outer\Inner inside a trait in %s on line %d From 21f7dfc1ebacd3c04276f18940ba7a6d4bebef11 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 31 Mar 2025 00:56:27 +0200 Subject: [PATCH 26/26] do not allow private/protected classes on interfaces --- Zend/zend_compile.c | 6 ++++++ ...interface_usage.phpt => interface_usage_001.phpt} | 0 tests/classes/inner_classes/interface_usage_002.phpt | 12 ++++++++++++ tests/classes/inner_classes/interface_usage_003.phpt | 12 ++++++++++++ 4 files changed, 30 insertions(+) rename tests/classes/inner_classes/{interface_usage.phpt => interface_usage_001.phpt} (100%) create mode 100644 tests/classes/inner_classes/interface_usage_002.phpt create mode 100644 tests/classes/inner_classes/interface_usage_003.phpt diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 4185f9ec0e47a..58e9c31a430a1 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9196,6 +9196,12 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) int propFlags = decl->attr & ZEND_ACC_PPP_MASK; decl->attr &= ~(ZEND_ACC_PPP_MASK | ZEND_ACC_FINAL | ZEND_ACC_READONLY | ZEND_ACC_ABSTRACT); + if (CG(active_class_entry)->ce_flags & ZEND_ACC_INTERFACE && propFlags & (ZEND_ACC_PRIVATE|ZEND_ACC_PROTECTED)) { + zend_error_noreturn(E_COMPILE_ERROR, "Cannot declare %s::$%s as %s in interface %s", + ZSTR_VAL(CG(active_class_entry)->name), ZSTR_VAL(unqualified_name), + zend_visibility_string(propFlags), ZSTR_VAL(CG(active_class_entry)->name)); + } + ce->required_scope = propFlags & (ZEND_ACC_PRIVATE | ZEND_ACC_PROTECTED) ? CG(active_class_entry) : NULL; ce->required_scope_absolute = propFlags & ZEND_ACC_PRIVATE ? true : false; ce->lexical_scope = CG(active_class_entry); diff --git a/tests/classes/inner_classes/interface_usage.phpt b/tests/classes/inner_classes/interface_usage_001.phpt similarity index 100% rename from tests/classes/inner_classes/interface_usage.phpt rename to tests/classes/inner_classes/interface_usage_001.phpt diff --git a/tests/classes/inner_classes/interface_usage_002.phpt b/tests/classes/inner_classes/interface_usage_002.phpt new file mode 100644 index 0000000000000..ece748e29a5ed --- /dev/null +++ b/tests/classes/inner_classes/interface_usage_002.phpt @@ -0,0 +1,12 @@ +--TEST-- +usage in an interface +--FILE-- + +--EXPECTF-- +Fatal error: Cannot declare Outer::$Inner as protected in interface Outer in %s on line %d diff --git a/tests/classes/inner_classes/interface_usage_003.phpt b/tests/classes/inner_classes/interface_usage_003.phpt new file mode 100644 index 0000000000000..81fcee6903181 --- /dev/null +++ b/tests/classes/inner_classes/interface_usage_003.phpt @@ -0,0 +1,12 @@ +--TEST-- +usage in an interface +--FILE-- + +--EXPECTF-- +Fatal error: Cannot declare Outer::$Inner as private in interface Outer in %s on line %d