Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Rfc: inner classes #18069

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5a8a98e
add new token
withinboredom Mar 8, 2025
716a401
create the new grammar
withinboredom Mar 8, 2025
1388dca
add new ast pieces
withinboredom Mar 8, 2025
c3cc41e
handle modifiers
withinboredom Mar 8, 2025
b9b93e6
handle compiling class declarations
withinboredom Mar 8, 2025
0ac2ef0
add initial implementation;
withinboredom Mar 8, 2025
17bd2cc
get return types working
withinboredom Mar 9, 2025
eef0b95
make ::class work
withinboredom Mar 9, 2025
f6006ad
fix failing test
withinboredom Mar 9, 2025
1b85087
add another class
withinboredom Mar 9, 2025
3d46bc2
add more tests to validate scope resolution keywords
withinboredom Mar 9, 2025
99abdcd
update reflection
withinboredom Mar 9, 2025
72e14ca
enable visibility
withinboredom Mar 9, 2025
0254635
add tests for autoloading
withinboredom Mar 9, 2025
458fca8
properly persist classes in opcache
withinboredom Mar 9, 2025
7a06f63
handle return type visibility enforcement
withinboredom Mar 9, 2025
eea3ad3
fix static member access
withinboredom Mar 11, 2025
d2b3256
move tests
withinboredom Mar 13, 2025
861da61
add more tests and fix access modifiers
withinboredom Mar 13, 2025
bf196a8
add support for lexical scope
withinboredom Mar 13, 2025
05aecfd
handle type visibility
withinboredom Mar 14, 2025
51f2fc5
fix opcache
withinboredom Mar 14, 2025
4fda62b
fix test
withinboredom Mar 14, 2025
2053a50
refine some error messages
withinboredom Mar 14, 2025
5f9db7b
add type check to return type
withinboredom Mar 14, 2025
7376eff
fix tests
withinboredom Mar 14, 2025
2092910
temporarily fix this test -- might want to undo this
withinboredom Mar 14, 2025
5f5d53f
clean up a bit and check readonly
withinboredom Mar 14, 2025
0ff7a33
handle visibility of methods
withinboredom Mar 14, 2025
b62121a
just do not cache -- more trouble than it is worth right now
withinboredom Mar 14, 2025
1f48e2b
properly handle lexical scope
withinboredom Mar 14, 2025
866bab3
handle long names
withinboredom Mar 14, 2025
b7737a2
handle more visibility
withinboredom Mar 14, 2025
ad8a4b4
handle constructor visibility
withinboredom Mar 15, 2025
405dab1
prevent static access
withinboredom Mar 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Zend/tests/errmsg/errmsg_027.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions Zend/zend.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ struct _zend_class_entry {
HashTable properties_info;
HashTable constants_table;

zend_class_entry *required_scope;
zend_class_entry *lexical_scope;
char required_scope_absolute;

ZEND_MAP_PTR_DEF(zend_class_mutable_data*, mutable_data);
zend_inheritance_cache_entry *inheritance_cache;

Expand Down
1 change: 1 addition & 0 deletions Zend/zend_ast.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ enum _zend_ast_kind {
ZEND_AST_YIELD,
ZEND_AST_COALESCE,
ZEND_AST_ASSIGN_COALESCE,
ZEND_AST_INNER_CLASS,

ZEND_AST_STATIC,
ZEND_AST_WHILE,
Expand Down
174 changes: 166 additions & 8 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -893,19 +893,20 @@ 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_INNER_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_INNER_CLASS) {
return ZEND_ACC_ABSTRACT;
}
break;
case T_FINAL:
if (target == ZEND_MODIFIER_TARGET_METHOD
|| target == ZEND_MODIFIER_TARGET_CONSTANT
|| target == ZEND_MODIFIER_TARGET_PROPERTY
|| target == ZEND_MODIFIER_TARGET_INNER_CLASS
|| target == ZEND_MODIFIER_TARGET_PROPERTY_HOOK) {
return ZEND_ACC_FINAL;
}
Expand Down Expand Up @@ -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_INNER_CLASS) {
member = "inner class";
} else {
ZEND_UNREACHABLE();
}
Expand Down Expand Up @@ -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_INNER_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;
}
/* }}} */
Expand Down Expand Up @@ -1760,8 +1794,45 @@ static uint32_t zend_get_class_fetch_type_ast(zend_ast *name_ast) /* {{{ */
}
/* }}} */

static bool zend_try_compile_const_expr_resolve_class_name(zval *zv, zend_ast *class_ast);

static zend_string *zend_resolve_nested_class_name(zend_ast *ast) /* {{{ */
{
ZEND_ASSERT(ast->kind == ZEND_AST_INNER_CLASS);

zend_ast *outer_class = ast->child[0];
zend_ast *inner_class = ast->child[1];

zend_string *outer_name, *inner_name, *full_name;

if (outer_class->kind == ZEND_AST_INNER_CLASS) {
outer_name = zend_resolve_nested_class_name(outer_class);
} else {
zval outer_class_name;
zend_try_compile_const_expr_resolve_class_name(&outer_class_name, outer_class);
outer_name = Z_STR(outer_class_name);
}

inner_name = zend_ast_get_str(inner_class);

full_name = zend_string_concat3(
ZSTR_VAL(outer_name), ZSTR_LEN(outer_name),
":>", 2,
ZSTR_VAL(inner_name), ZSTR_LEN(inner_name)
);

zend_string_release(outer_name);

return full_name;
}
/* }}} */

static zend_string *zend_resolve_const_class_name_reference(zend_ast *ast, const char *type)
{
if (ast->kind == ZEND_AST_INNER_CLASS) {
return zend_resolve_nested_class_name(ast);
}

zend_string *class_name = zend_ast_get_str(ast);
if (ZEND_FETCH_CLASS_DEFAULT != zend_get_class_fetch_type_ast(ast)) {
zend_error_noreturn(E_COMPILE_ERROR,
Expand Down Expand Up @@ -1792,6 +1863,11 @@ static bool zend_try_compile_const_expr_resolve_class_name(zval *zv, zend_ast *c
uint32_t fetch_type;
zval *class_name;

if (class_ast->kind == ZEND_AST_INNER_CLASS) {
ZVAL_STR(zv, zend_resolve_nested_class_name(class_ast));
return 1;
}

if (class_ast->kind != ZEND_AST_ZVAL) {
return 0;
}
Expand Down Expand Up @@ -2844,10 +2920,42 @@ static inline void zend_set_class_name_op1(zend_op *opline, znode *class_node) /
}
/* }}} */

static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t fetch_flags);

static void zend_compile_inner_class_ref(znode *result, zend_ast *ast, uint32_t fetch_flags) /* {{{ */
{
zend_ast *outer_class = ast->child[0];
zend_ast *inner_class = ast->child[1];

znode outer_node, inner_node;

// handle nesting
if (outer_class->kind == ZEND_AST_INNER_CLASS) {
zend_compile_inner_class_ref(&outer_node, outer_class, fetch_flags);
} else {
zend_compile_class_ref(&outer_node, outer_class, fetch_flags | ZEND_FETCH_CLASS_OUTER);
}

if (inner_class->kind == ZEND_AST_ZVAL && Z_TYPE_P(zend_ast_get_zval(inner_class)) == IS_STRING) {
ZVAL_STR(&inner_node.u.constant, zend_string_dup(Z_STR_P(zend_ast_get_zval(inner_class)), 0));
inner_node.op_type = IS_CONST;
} else {
zend_compile_expr(&inner_node, inner_class);
}

zend_emit_op(result, ZEND_FETCH_INNER_CLASS, &outer_node, &inner_node);
}
/* }}} */

static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t fetch_flags) /* {{{ */
{
uint32_t fetch_type;

if (name_ast->kind == ZEND_AST_INNER_CLASS) {
zend_compile_inner_class_ref(result, name_ast, fetch_flags);
return;
}

if (name_ast->kind != ZEND_AST_ZVAL) {
znode name_node;

Expand Down Expand Up @@ -2891,10 +2999,12 @@ static void zend_compile_class_ref(znode *result, zend_ast *name_ast, uint32_t f
if (ZEND_FETCH_CLASS_DEFAULT == fetch_type) {
result->op_type = IS_CONST;
ZVAL_STR(&result->u.constant, zend_resolve_class_name_ast(name_ast));
} else if (fetch_flags & ZEND_FETCH_CLASS_OUTER && fetch_type == ZEND_FETCH_CLASS_STATIC) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot use the static modifier on an inner class");
} else {
zend_ensure_valid_class_fetch_type(fetch_type);
result->op_type = IS_UNUSED;
result->u.op.num = fetch_type | fetch_flags;
result->u.op.num = fetch_type | (fetch_flags & ~ZEND_FETCH_CLASS_OUTER);
}
}
/* }}} */
Expand Down Expand Up @@ -7316,6 +7426,9 @@ static zend_type zend_compile_typename_ex(
ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_INTERSECTION_BIT;
ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_ARENA_BIT;
}
} else if (ast->kind == ZEND_AST_INNER_CLASS) {
zend_string *name = zend_resolve_nested_class_name(ast);
return (zend_type) ZEND_TYPE_INIT_CLASS(name, /* allow null */ false, 0);
} else {
type = zend_compile_single_typename(ast);
}
Expand Down Expand Up @@ -9057,10 +9170,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";
Expand All @@ -9070,7 +9179,51 @@ 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) && CG(active_op_array)->function_name) {
zend_error_noreturn(E_COMPILE_ERROR, "Class declarations may not be declared inside functions");
}

if (CG(active_class_entry)) {
// rename the inner class so we may reference it by name
name = zend_string_concat3(
ZSTR_VAL(CG(active_class_entry)->name), ZSTR_LEN(CG(active_class_entry)->name),
":>", 2,
ZSTR_VAL(unqualified_name), ZSTR_LEN(unqualified_name)
);

// configure the current ce->flags for a nested class. This should only include:
// - final
// - readonly
// - abstract
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;
}

// configure for a nested class. This should only include:
// - public
// - private
// - protected
int propFlags = decl->attr & (ZEND_ACC_PUBLIC|ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE);
// remove the flags from attrs
decl->attr &= ~(ZEND_ACC_PUBLIC|ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE|ZEND_ACC_FINAL|ZEND_ACC_ABSTRACT|ZEND_ACC_READONLY);

// if a class is private or protected, we need to require the correct scope
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);

// ensure the class is treated as a top-level class and not an anon class
toplevel = true;
} else {
name = zend_prefix_with_ns(unqualified_name);
ce->required_scope = NULL;
ce->lexical_scope = NULL;
}
name = zend_new_interned_string(name);
lcname = zend_string_tolower(name);

Expand All @@ -9088,6 +9241,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 = NULL;
do {
zend_tmp_string_release(name);
zend_tmp_string_release(lcname);
Expand Down Expand Up @@ -11697,6 +11852,9 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */
case ZEND_AST_MATCH:
zend_compile_match(result, ast);
return;
case ZEND_AST_INNER_CLASS:
zend_compile_inner_class_ref(result, ast, ZEND_FETCH_CLASS_EXCEPTION);
return;
default:
ZEND_ASSERT(0 /* not supported */);
}
Expand Down
2 changes: 2 additions & 0 deletions Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,7 @@ typedef enum {
ZEND_MODIFIER_TARGET_CONSTANT,
ZEND_MODIFIER_TARGET_CPP,
ZEND_MODIFIER_TARGET_PROPERTY_HOOK,
ZEND_MODIFIER_TARGET_INNER_CLASS,
} zend_modifier_target;

/* Used during AST construction */
Expand Down Expand Up @@ -1020,6 +1021,7 @@ ZEND_API zend_string *zend_type_to_string(zend_type type);
#define ZEND_FETCH_CLASS_AUTO 4
#define ZEND_FETCH_CLASS_INTERFACE 5
#define ZEND_FETCH_CLASS_TRAIT 6
#define ZEND_FETCH_CLASS_OUTER 7
#define ZEND_FETCH_CLASS_MASK 0x0f
#define ZEND_FETCH_CLASS_NO_AUTOLOAD 0x80
#define ZEND_FETCH_CLASS_SILENT 0x0100
Expand Down
54 changes: 54 additions & 0 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -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_always_inline bool zend_check_class_visibility(const zend_class_entry *ce, const zend_property_info *info, uint32_t current_visibility) {
// a public class is always visible
if (!ce->required_scope) {
return 1;
}

// 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) {
if (current_visibility & ZEND_ACC_PUBLIC) {
zend_type_error("Cannot assign protected %s to higher visibile property %s::%s",
ZSTR_VAL(ce->name),
ZSTR_VAL(info->ce->name),
zend_get_unmangled_property_name(info->name));
return 0;
}

return 1;
}

// 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 && current_visibility & ZEND_ACC_PRIVATE) {
return 1;
}

zend_type_error("Cannot assign private %s to higher visibile property %s::%s",
ZSTR_VAL(ce->name),
ZSTR_VAL(info->ce->name),
zend_get_unmangled_property_name(info->name));

return 0;
}

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_class_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;
}
Expand Down Expand Up @@ -1196,6 +1233,23 @@ static zend_always_inline bool zend_check_type_slow(
}
} else {
ce = zend_fetch_ce_from_cache_slot(cache_slot, type);

// verify that the class is being used in the correct scope
if (ce && ce->required_scope) {
zend_class_entry *scope = zend_get_executed_scope();
if (ce->required_scope_absolute && scope != ce->required_scope) {
if (scope == NULL) {
zend_error(E_ERROR, "Private inner class %s cannot be used as a type declaration in the global scope", ZSTR_VAL(ce->name));
}

zend_error(E_ERROR, "Private inner class %s cannot be used as a type declaration in the scope of %s", ZSTR_VAL(ce->name), ZSTR_VAL(scope->name));
} else if (scope == NULL) {
zend_error(E_ERROR, "Protected inner class %s cannot be used as a type declaration in the global scope", ZSTR_VAL(ce->name));
} else if (!instanceof_function(scope, ce->required_scope)) {
zend_error(E_ERROR, "Protected inner class %s cannot be used as a type declaration in the scope of %s", ZSTR_VAL(ce->name), ZSTR_VAL(scope->name));
}
}

/* If we have a CE we check if it satisfies the type constraint,
* otherwise it will check if a standard type satisfies it. */
if (ce && instanceof_function(Z_OBJCE_P(arg), ce)) {
Expand Down
Loading
Loading