diff --git a/docs/internals/the-parser.md b/docs/internals/the-parser.md index efa9d3d3bc..f23c5d3d41 100644 --- a/docs/internals/the-parser.md +++ b/docs/internals/the-parser.md @@ -98,6 +98,7 @@ Things that have a value: | `BufferNew { element_type, len }` | `buffer_new(256)` | Compiler extension for contiguous hot-path buffers | | `MagicConstant(MagicConstant)` | `__DIR__`, `__CLASS__` | Parsed from case-insensitive magic-constant tokens. `__LINE__` is lowered immediately to `IntLiteral`; the remaining magic constants are lowered by `src/magic_constants.rs` before type checking. | | `ClassConstant { receiver }` | `MyClass::class`, `\App\C::class`, `self::class`, `parent::class`, `static::class` | The PHP `::class` reflection literal. Codegen lowers it to a string literal carrying the fully-qualified class name. `static::class` follows late static binding. | +| `ObjectClassName { object }` | `$object::class`, `make_object()::class` | Runtime `::class` lookup for an object-valued expression. The object expression remains structural so later passes evaluate it once and read its concrete runtime class. | | `ScopedConstantAccess { receiver, name }` | `MyClass::LIMIT`, `self::DEFAULT_SIZE` | User-declared class constant access through `::`; later phases resolve the receiver and constant metadata. | ### Statements (`Stmt`) @@ -391,6 +392,7 @@ Before looking for infix operators, the parser handles **prefix** constructs — | `new` + `$var` + `(` | Parse dynamic object instantiation → `NewDynamic` (class named by a runtime variable) | | `new` + `self` / `static` / `parent` + `(` | Parse scoped object instantiation → `NewScopedObject` | | `::class` | Parse `MyClass::class`, `\App\C::class`, `self::class`, `parent::class`, `static::class` → `ClassConstant` | +| `::class` | Parse `$object::class`, `make_object()::class` → `ObjectClassName` | | `$this` | Return `This` node | | `...` + expr | Parse spread/unpack → `Spread` | | `ptr_cast` + `` + `(` | Parse pointer cast syntax → `PtrCast` | diff --git a/docs/php/classes.md b/docs/php/classes.md index 510a012dc6..e69ccfcf30 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -539,7 +539,9 @@ Called with `::`, no `$this`. ## Class name reflection (`::class`) -`::class` returns the fully-qualified class name as a string at compile time. +`::class` returns a fully-qualified class name as a string. Named and scoped +receivers are resolved without constructing an object; an object-valued receiver +reads the concrete class from the object at runtime. ```php describe() . "\n"; +// Object receivers resolve the concrete runtime class, like get_class($named). +echo "Named logger runtime class: " . $named::class . "\n"; // Inheritance + parent::class class Base { diff --git a/src/autoload/walk.rs b/src/autoload/walk.rs index f8a8dcd84b..8d1268c2fc 100644 --- a/src/autoload/walk.rs +++ b/src/autoload/walk.rs @@ -550,7 +550,8 @@ fn collect_refs_expr(expr: &Expr, out: &mut HashSet) { } } ExprKind::PropertyAccess { object, .. } - | ExprKind::NullsafePropertyAccess { object, .. } => collect_refs_expr(object, out), + | ExprKind::NullsafePropertyAccess { object, .. } + | ExprKind::ObjectClassName { object } => collect_refs_expr(object, out), ExprKind::MethodCall { object, args, .. } | ExprKind::NullsafeMethodCall { object, args, .. } => { collect_refs_expr(object, out); diff --git a/src/codegen_support/program_usage/required_classes/collect.rs b/src/codegen_support/program_usage/required_classes/collect.rs index 1b79caef13..1c04359338 100644 --- a/src/codegen_support/program_usage/required_classes/collect.rs +++ b/src/codegen_support/program_usage/required_classes/collect.rs @@ -462,6 +462,9 @@ fn collect_required_class_names_in_expr(expr: &Expr, names: &mut HashSet names.insert(name.as_str().to_string()); } } + ExprKind::ObjectClassName { object } => { + collect_required_class_names_in_expr(object, names); + } ExprKind::ScopedConstantAccess { receiver, .. } => { if let crate::parser::ast::StaticReceiver::Named(name) = receiver { names.insert(name.as_str().to_string()); diff --git a/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs b/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs index c2896b238f..c7a50a78b6 100644 --- a/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs +++ b/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs @@ -163,7 +163,8 @@ fn expr_has_dynamic_instanceof(expr: &Expr) -> bool { | ExprKind::Cast { expr, .. } | ExprKind::PtrCast { expr, .. } | ExprKind::NamedArg { value: expr, .. } - | ExprKind::BufferNew { len: expr, .. } => expr_has_dynamic_instanceof(expr), + | ExprKind::BufferNew { len: expr, .. } + | ExprKind::ObjectClassName { object: expr } => expr_has_dynamic_instanceof(expr), ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => { expr_has_dynamic_instanceof(value) || expr_has_dynamic_instanceof(default) diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index e59b6ea1bc..744645fcf7 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -468,6 +468,7 @@ fn expr_has_regex_call(expr: &Expr) -> bool { is_regex_builtin_name(name.as_str()) } ExprKind::FirstClassCallable(CallableTarget::StaticMethod { .. }) => false, + ExprKind::ObjectClassName { object } => expr_has_regex_call(object), ExprKind::StaticPropertyAccess { receiver, .. } | ExprKind::ClassConstant { receiver } | ExprKind::ScopedConstantAccess { receiver, .. } => { @@ -794,6 +795,7 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { expr_needs_descriptor_invoker(object) } ExprKind::FirstClassCallable(_) => false, + ExprKind::ObjectClassName { object } => expr_needs_descriptor_invoker(object), ExprKind::StaticPropertyAccess { .. } | ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => false, diff --git a/src/eval_aot.rs b/src/eval_aot.rs index 9dde8f9b65..254398b087 100644 --- a/src/eval_aot.rs +++ b/src/eval_aot.rs @@ -1440,6 +1440,7 @@ fn expr_fallback_reason(expr: &Expr) -> Option { | ExprKind::NullsafeMethodCall { .. } | ExprKind::StaticMethodCall { .. } | ExprKind::ClassConstant { .. } + | ExprKind::ObjectClassName { .. } | ExprKind::ScopedConstantAccess { .. } | ExprKind::This => Some(EvalAotFallbackReason::ObjectOrMemberAccess), ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) | ExprKind::Spread(_) => { @@ -2566,6 +2567,7 @@ fn collect_expr_scope_access(expr: &Expr, access: &mut EvalScopeAccess) { | ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } | ExprKind::MagicConstant(_) => {} + ExprKind::ObjectClassName { object } => collect_expr_scope_access(object, access), ExprKind::Variable(name) | ExprKind::PreIncrement(name) | ExprKind::PostIncrement(name) diff --git a/src/image_prelude/detect.rs b/src/image_prelude/detect.rs index ef2fa5d95c..870bc3fee5 100644 --- a/src/image_prelude/detect.rs +++ b/src/image_prelude/detect.rs @@ -355,6 +355,7 @@ fn expr_refs_image(expr: &Expr) -> bool { } ExprKind::ClassConstant { receiver } | ExprKind::ScopedConstantAccess { receiver, .. } => receiver_refs_image(receiver), + ExprKind::ObjectClassName { object } => expr_refs_image(object), ExprKind::NewScopedObject { receiver, args } => { receiver_refs_image(receiver) || args.iter().any(expr_refs_image) } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 4f96d76186..38b9c540f5 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -166,6 +166,7 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe ExprKind::PtrCast { target_type, expr: inner } => lower_ptr_cast(ctx, target_type, inner, expr), ExprKind::BufferNew { element_type, len } => lower_buffer_new(ctx, element_type, len, expr), ExprKind::ClassConstant { receiver } => lower_class_constant(ctx, receiver, expr), + ExprKind::ObjectClassName { object } => lower_object_class_name(ctx, object, expr), ExprKind::ScopedConstantAccess { receiver, name } => { lower_scoped_constant(ctx, receiver, name, expr) } @@ -713,7 +714,10 @@ fn expr_can_reset_concat_storage(expr: &Expr) -> bool { | ExprKind::NamedArg { value: inner, .. } | ExprKind::Spread(inner) | ExprKind::PtrCast { expr: inner, .. } - | ExprKind::BufferNew { len: inner, .. } => expr_can_reset_concat_storage(inner), + | ExprKind::BufferNew { len: inner, .. } + | ExprKind::ObjectClassName { object: inner } => { + expr_can_reset_concat_storage(inner) + } ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => { expr_can_reset_concat_storage(value) || expr_can_reset_concat_storage(default) @@ -9421,6 +9425,7 @@ fn expr_contains_eval_call(expr: &Expr) -> bool { | ExprKind::Cast { expr, .. } | ExprKind::PtrCast { expr, .. } | ExprKind::BufferNew { len: expr, .. } + | ExprKind::ObjectClassName { object: expr } | ExprKind::YieldFrom(expr) => expr_contains_eval_call(expr), ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } @@ -14433,6 +14438,23 @@ fn lower_class_constant(ctx: &mut LoweringContext<'_, '_>, receiver: &StaticRece ) } +/// Lowers an object-valued `::class` receiver through the runtime class-name lookup. +fn lower_object_class_name( + ctx: &mut LoweringContext<'_, '_>, + object: &Expr, + expr: &Expr, +) -> LoweredValue { + let object = lower_expr(ctx, object); + emit_builtin_call_value( + ctx, + "get_class", + vec![object.value], + PhpType::Str, + expr.span, + None, + ) +} + /// Lowers a scoped constant read. fn lower_scoped_constant(ctx: &mut LoweringContext<'_, '_>, receiver: &StaticReceiver, name: &str, expr: &Expr) -> LoweredValue { let class_name = scoped_constant_receiver_name(ctx, receiver); diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index 9ed82a0db6..a8dbc39e2a 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -343,6 +343,7 @@ fn lowers_every_expr_variant_smoke() { expr(ExprKind::ClassConstant { receiver: StaticReceiver::Self_ }), expr(ExprKind::ClassConstant { receiver: StaticReceiver::Static }), expr(ExprKind::ClassConstant { receiver: StaticReceiver::Parent }), + expr(ExprKind::ObjectClassName { object: Box::new(object.clone()) }), expr(ExprKind::ScopedConstantAccess { receiver: StaticReceiver::Named(name("C")), name: "K".to_string() }), expr(ExprKind::NewScopedObject { receiver: StaticReceiver::Named(name("C")), args: Vec::new() }), expr(ExprKind::MagicConstant(MagicConstant::File)), diff --git a/src/list_id_prelude/detect.rs b/src/list_id_prelude/detect.rs index 8b03391fb0..e3b3847fa3 100644 --- a/src/list_id_prelude/detect.rs +++ b/src/list_id_prelude/detect.rs @@ -250,6 +250,7 @@ fn expr_refs_listid(expr: &Expr) -> bool { ExprKind::StaticPropertyAccess { .. } => false, ExprKind::BufferNew { len, .. } => expr_refs_listid(len), ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => false, + ExprKind::ObjectClassName { object } => expr_refs_listid(object), ExprKind::NewScopedObject { args, .. } => args.iter().any(expr_refs_listid), ExprKind::Yield { key, value } => { key.as_deref().is_some_and(expr_refs_listid) diff --git a/src/magic_constants/walker/exprs.rs b/src/magic_constants/walker/exprs.rs index c474800e19..495b9f4c19 100644 --- a/src/magic_constants/walker/exprs.rs +++ b/src/magic_constants/walker/exprs.rs @@ -274,6 +274,9 @@ pub(super) fn walk_expr(expr: Expr, pass: &mut P) -> Expr { len: Box::new(walk_expr(*len, pass)), }, ExprKind::ClassConstant { receiver } => ExprKind::ClassConstant { receiver }, + ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName { + object: Box::new(walk_expr(*object, pass)), + }, ExprKind::ScopedConstantAccess { receiver, name } => { ExprKind::ScopedConstantAccess { receiver, name } } diff --git a/src/name_resolver/expressions.rs b/src/name_resolver/expressions.rs index 42dce9fdc4..c56a623b44 100644 --- a/src/name_resolver/expressions.rs +++ b/src/name_resolver/expressions.rs @@ -264,6 +264,9 @@ pub(super) fn resolve_expr( _ => receiver.clone(), }, }, + ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName { + object: Box::new(resolve_expr(object, current_namespace, imports, symbols)), + }, ExprKind::ScopedConstantAccess { receiver, name } => ExprKind::ScopedConstantAccess { receiver: match receiver { StaticReceiver::Named(name) => StaticReceiver::Named(resolved_name( diff --git a/src/optimize/control/prune/expr.rs b/src/optimize/control/prune/expr.rs index e93cd07a7c..6cf2894a2a 100644 --- a/src/optimize/control/prune/expr.rs +++ b/src/optimize/control/prune/expr.rs @@ -253,6 +253,9 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr { len: Box::new(prune_expr(*len)), }, ExprKind::ClassConstant { receiver } => ExprKind::ClassConstant { receiver }, + ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName { + object: Box::new(prune_expr(*object)), + }, ExprKind::ScopedConstantAccess { receiver, name } => { ExprKind::ScopedConstantAccess { receiver, name } } diff --git a/src/optimize/effects.rs b/src/optimize/effects.rs index 327baa599c..103ce30f0a 100644 --- a/src/optimize/effects.rs +++ b/src/optimize/effects.rs @@ -346,6 +346,7 @@ pub(super) fn expr_effect(expr: &Expr) -> Effect { ExprKind::FirstClassCallable(target) => callable_target_effect(target), ExprKind::BufferNew { len, .. } => expr_effect(len).with_side_effects(), ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => Effect::PURE, + ExprKind::ObjectClassName { object } => expr_effect(object), ExprKind::NewScopedObject { args, .. } => combine_effects(args.iter().map(expr_effect)) .with_side_effects() .with_may_throw(), diff --git a/src/optimize/fold/expr.rs b/src/optimize/fold/expr.rs index 72d7704326..a51d9cbcce 100644 --- a/src/optimize/fold/expr.rs +++ b/src/optimize/fold/expr.rs @@ -380,6 +380,9 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { len: Box::new(fold_expr(*len)), }, ExprKind::ClassConstant { receiver } => ExprKind::ClassConstant { receiver }, + ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName { + object: Box::new(fold_expr(*object)), + }, ExprKind::ScopedConstantAccess { receiver, name } => { ExprKind::ScopedConstantAccess { receiver, name } } diff --git a/src/optimize/propagate/expr.rs b/src/optimize/propagate/expr.rs index ddfaf17acd..b7c975645c 100644 --- a/src/optimize/propagate/expr.rs +++ b/src/optimize/propagate/expr.rs @@ -361,6 +361,9 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr { len: Box::new(propagate_expr(*len, env)), }, ExprKind::ClassConstant { receiver } => ExprKind::ClassConstant { receiver }, + ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName { + object: Box::new(propagate_expr(*object, env)), + }, ExprKind::ScopedConstantAccess { receiver, name } => { ExprKind::ScopedConstantAccess { receiver, name } } diff --git a/src/optimize/propagate/invalidation.rs b/src/optimize/propagate/invalidation.rs index f645fc4ed6..377b5b5ebb 100644 --- a/src/optimize/propagate/invalidation.rs +++ b/src/optimize/propagate/invalidation.rs @@ -95,6 +95,7 @@ pub(crate) fn expr_invalidation(expr: &Expr) -> Invalidation { | ExprKind::This | ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => Invalidation::none(), + ExprKind::ObjectClassName { object } => expr_invalidation(object), // Creating a closure executes nothing, but its by-ref captures alias // the outer variables from this point on: any existing fact for them // must die here (the volatility ledger only blocks *future* facts). diff --git a/src/parser/ast/expr.rs b/src/parser/ast/expr.rs index 127c5e5449..986e8562de 100644 --- a/src/parser/ast/expr.rs +++ b/src/parser/ast/expr.rs @@ -215,6 +215,12 @@ pub enum ExprKind { ClassConstant { receiver: StaticReceiver, }, + /// `$object::class` or another object-valued expression followed by `::class`. + /// Unlike `ClassConstant`, this resolves the concrete runtime class of the + /// evaluated object instead of naming a compile-time static receiver. + ObjectClassName { + object: Box, + }, /// Access to a user-declared class constant: `MyClass::FOO`, /// `self::FOO`, `parent::FOO`, `static::FOO`. Resolved at type-check /// time by looking up the constant in the receiver's class info. diff --git a/src/parser/expr/assignment_targets.rs b/src/parser/expr/assignment_targets.rs index 5dbd1c6521..f1ae95cfc5 100644 --- a/src/parser/expr/assignment_targets.rs +++ b/src/parser/expr/assignment_targets.rs @@ -350,7 +350,10 @@ fn collect_assignment_target_dependencies(expr: &Expr, dependencies: &mut HashSe | ExprKind::Cast { expr: value, .. } | ExprKind::PtrCast { expr: value, .. } | ExprKind::NamedArg { value, .. } - | ExprKind::Spread(value) => collect_assignment_target_dependencies(value, dependencies), + | ExprKind::Spread(value) + | ExprKind::ObjectClassName { object: value } => { + collect_assignment_target_dependencies(value, dependencies) + } ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => { collect_assignment_target_dependencies(value, dependencies); collect_assignment_target_dependencies(default, dependencies); @@ -483,7 +486,10 @@ fn expr_may_write_dependency(expr: &Expr, dependencies: &HashSet) -> boo | ExprKind::Cast { expr: value, .. } | ExprKind::PtrCast { expr: value, .. } | ExprKind::NamedArg { value, .. } - | ExprKind::Spread(value) => expr_may_write_dependency(value, dependencies), + | ExprKind::Spread(value) + | ExprKind::ObjectClassName { object: value } => { + expr_may_write_dependency(value, dependencies) + } ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => { expr_may_write_dependency(value, dependencies) || expr_may_write_dependency(default, dependencies) @@ -695,6 +701,7 @@ fn expr_contains_equivalent(expr: &Expr, needle: &Expr) -> bool { | ExprKind::PtrCast { expr: value, .. } | ExprKind::NamedArg { value, .. } | ExprKind::Spread(value) + | ExprKind::ObjectClassName { object: value } | ExprKind::YieldFrom(value) => expr_contains_equivalent(value, needle), ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => { expr_contains_equivalent(value, needle) || expr_contains_equivalent(default, needle) diff --git a/src/parser/expr/pratt.rs b/src/parser/expr/pratt.rs index da7b143a3d..5025c0ea28 100644 --- a/src/parser/expr/pratt.rs +++ b/src/parser/expr/pratt.rs @@ -99,6 +99,17 @@ fn parse_expr_bp_inner( // `call_user_func([$cls, $method], ...args)`, reusing the runtime dispatch path. let span = tokens[*pos].1; *pos += 1; // consume '::' + if matches!(tokens.get(*pos).map(|(token, _)| token), Some(Token::Class)) { + *pos += 1; + let span = crate::parser::expr::span_through_prev_token(tokens, *pos, span); + lhs = Expr::new( + ExprKind::ObjectClassName { + object: Box::new(lhs), + }, + span, + ); + continue; + } let member = match tokens.get(*pos).map(|(token, s)| (token.clone(), *s)) { Some((Token::Identifier(name), name_span)) => { *pos += 1; diff --git a/src/parser/expr/prefix_complex.rs b/src/parser/expr/prefix_complex.rs index 587010846d..0c74ffdf98 100644 --- a/src/parser/expr/prefix_complex.rs +++ b/src/parser/expr/prefix_complex.rs @@ -351,6 +351,7 @@ fn collect_arrow_expr_captures( | ExprKind::Spread(inner) | ExprKind::PtrCast { expr: inner, .. } | ExprKind::Cast { expr: inner, .. } + | ExprKind::ObjectClassName { object: inner } | ExprKind::YieldFrom(inner) => collect_arrow_expr_captures(inner, bound, seen, captures), ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => { collect_arrow_expr_captures(value, bound, seen, captures); diff --git a/src/pdo_prelude/detect.rs b/src/pdo_prelude/detect.rs index 3505ce0406..1c6f0b0fd0 100644 --- a/src/pdo_prelude/detect.rs +++ b/src/pdo_prelude/detect.rs @@ -287,6 +287,7 @@ fn expr_refs_pdo(expr: &Expr) -> bool { } ExprKind::ClassConstant { receiver } | ExprKind::ScopedConstantAccess { receiver, .. } => receiver_refs_pdo(receiver), + ExprKind::ObjectClassName { object } => expr_refs_pdo(object), ExprKind::NewScopedObject { receiver, args } => { receiver_refs_pdo(receiver) || args.iter().any(expr_refs_pdo) } diff --git a/src/resolver/contains.rs b/src/resolver/contains.rs index fd1d3d8ce0..0605c9b30c 100644 --- a/src/resolver/contains.rs +++ b/src/resolver/contains.rs @@ -237,6 +237,7 @@ fn expr_has_includes(expr: &Expr) -> bool { ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => { expr_has_includes(object) } + ExprKind::ObjectClassName { object } => expr_has_includes(object), ExprKind::StringLiteral(_) | ExprKind::IntLiteral(_) | ExprKind::FloatLiteral(_) diff --git a/src/resolver/discovery/exprs.rs b/src/resolver/discovery/exprs.rs index 47d0c0916a..b60c22c4f4 100644 --- a/src/resolver/discovery/exprs.rs +++ b/src/resolver/discovery/exprs.rs @@ -201,6 +201,9 @@ pub(super) fn discover_expr( ExprKind::FirstClassCallable(crate::parser::ast::CallableTarget::Method { object, .. }) => { discover_expr(object, base_dir, loaded_paths, include_chain, state, output)?; } + ExprKind::ObjectClassName { object } => { + discover_expr(object, base_dir, loaded_paths, include_chain, state, output)?; + } ExprKind::StringLiteral(_) | ExprKind::IntLiteral(_) | ExprKind::FloatLiteral(_) diff --git a/src/strict_php/audit.rs b/src/strict_php/audit.rs index 65e3806c8b..a3ca67b9fa 100644 --- a/src/strict_php/audit.rs +++ b/src/strict_php/audit.rs @@ -509,6 +509,7 @@ fn audit_expr(expr: &Expr, errors: &mut Vec) { | ExprKind::Print(inner) | ExprKind::Spread(inner) | ExprKind::Clone(inner) + | ExprKind::ObjectClassName { object: inner } | ExprKind::YieldFrom(inner) => audit_expr(inner, errors), ExprKind::NullCoalesce { value, default } => { audit_expr(value, errors); diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index 1a1bf086d0..19bc31deae 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -614,6 +614,24 @@ impl Checker { self.validate_class_constant_receiver(receiver, expr.span)?; Ok(PhpType::Str) } + ExprKind::ObjectClassName { object } => { + let object_type = self.infer_type(object, env)?; + let object_only = match &object_type { + PhpType::Object(_) => true, + PhpType::Union(members) => { + !members.is_empty() + && members.iter().all(|member| matches!(member, PhpType::Object(_))) + } + _ => false, + }; + if !object_only { + return Err(CompileError::new( + expr.span, + &format!("Cannot use \"::class\" on {}", object_type), + )); + } + Ok(PhpType::Str) + } ExprKind::ScopedConstantAccess { receiver, name } => { self.infer_scoped_constant_access(receiver, name, expr) } diff --git a/src/types/checker/inference/syntactic.rs b/src/types/checker/inference/syntactic.rs index d0a8b08213..ab9eefc3af 100644 --- a/src/types/checker/inference/syntactic.rs +++ b/src/types/checker/inference/syntactic.rs @@ -366,7 +366,9 @@ pub fn infer_expr_type_syntactic(expr: &Expr) -> PhpType { PhpType::Object(fallback_class.as_str().to_string()) } ExprKind::NewScopedObject { .. } => PhpType::Object(String::new()), - ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => PhpType::Str, + ExprKind::ClassConstant { .. } + | ExprKind::ObjectClassName { .. } + | ExprKind::ScopedConstantAccess { .. } => PhpType::Str, ExprKind::This => PhpType::Object(String::new()), ExprKind::Closure { .. } | ExprKind::FirstClassCallable(_) => PhpType::Callable, ExprKind::PtrCast { target_type, .. } => PhpType::Pointer(Some(target_type.clone())), diff --git a/src/types/checker/schema/classes/constants.rs b/src/types/checker/schema/classes/constants.rs index 54f1bf2b3b..43518cad13 100644 --- a/src/types/checker/schema/classes/constants.rs +++ b/src/types/checker/schema/classes/constants.rs @@ -303,6 +303,9 @@ fn rewrite_expr( ExprKind::ClassConstant { receiver } => ExprKind::ClassConstant { receiver: rewrite_constant_receiver(receiver, class_name, parent_name, expr.span)?, }, + ExprKind::ObjectClassName { object } => ExprKind::ObjectClassName { + object: Box::new(rewrite_expr(object, class_name, parent_name)?), + }, ExprKind::ScopedConstantAccess { receiver, name } => { ExprKind::ScopedConstantAccess { receiver: rewrite_constant_receiver(receiver, class_name, parent_name, expr.span)?, diff --git a/src/types/checker/yield_validation/validate.rs b/src/types/checker/yield_validation/validate.rs index 22e52e8d1a..2cb35062b5 100644 --- a/src/types/checker/yield_validation/validate.rs +++ b/src/types/checker/yield_validation/validate.rs @@ -390,6 +390,7 @@ fn visit_expr(expr: &Expr, st: &mut State) { } ExprKind::PropertyAccess { object, .. } | ExprKind::NullsafePropertyAccess { object, .. } => visit_expr(object, st), + ExprKind::ObjectClassName { object } => visit_expr(object, st), ExprKind::DynamicPropertyAccess { object, property } | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { visit_expr(object, st); diff --git a/src/types/return_alias.rs b/src/types/return_alias.rs index f21903b02d..4a5991dda6 100644 --- a/src/types/return_alias.rs +++ b/src/types/return_alias.rs @@ -547,6 +547,7 @@ fn expr_alias(expr: &Expr, state: &HashMap) -> ReturnArg | ExprKind::BitNot(_) | ExprKind::PtrCast { .. } | ExprKind::ClassConstant { .. } + | ExprKind::ObjectClassName { .. } | ExprKind::MagicConstant(_) => ReturnArgAlias::None, _ => ReturnArgAlias::Unknown, } @@ -708,6 +709,7 @@ fn apply_expr_effects(expr: &Expr, state: &mut HashMap) } ExprKind::NamedArg { value, .. } => apply_expr_effects(value, state), ExprKind::BufferNew { len, .. } => apply_expr_effects(len, state), + ExprKind::ObjectClassName { object } => apply_expr_effects(object, state), ExprKind::Yield { key, value } => { if let Some(key) = key { apply_expr_effects(key, state); diff --git a/src/types/warnings/expr_reads.rs b/src/types/warnings/expr_reads.rs index 96b1ebe85d..245aa24e86 100644 --- a/src/types/warnings/expr_reads.rs +++ b/src/types/warnings/expr_reads.rs @@ -204,6 +204,7 @@ pub(super) fn collect_expr_reads( } ExprKind::StaticPropertyAccess { .. } => {}, ExprKind::BufferNew { len, .. } => collect_expr_reads(len, scope, warnings), + ExprKind::ObjectClassName { object } => collect_expr_reads(object, scope, warnings), ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => {} ExprKind::NewScopedObject { args, .. } => { for arg in args { diff --git a/src/tz_prelude/detect.rs b/src/tz_prelude/detect.rs index 4693863b4a..4ef63e34e8 100644 --- a/src/tz_prelude/detect.rs +++ b/src/tz_prelude/detect.rs @@ -239,6 +239,7 @@ fn expr_refs_tz(expr: &Expr) -> bool { ExprKind::StaticPropertyAccess { .. } => false, ExprKind::BufferNew { len, .. } => expr_refs_tz(len), ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => false, + ExprKind::ObjectClassName { object } => expr_refs_tz(object), ExprKind::NewScopedObject { args, .. } => args.iter().any(expr_refs_tz), ExprKind::Yield { key, value } => { key.as_deref().is_some_and(expr_refs_tz) diff --git a/src/var_export_prelude/detect.rs b/src/var_export_prelude/detect.rs index bb51010027..aa70f71e2d 100644 --- a/src/var_export_prelude/detect.rs +++ b/src/var_export_prelude/detect.rs @@ -231,6 +231,7 @@ fn expr_refs_ve(expr: &Expr) -> bool { ExprKind::StaticPropertyAccess { .. } => false, ExprKind::BufferNew { len, .. } => expr_refs_ve(len), ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => false, + ExprKind::ObjectClassName { object } => expr_refs_ve(object), ExprKind::NewScopedObject { args, .. } => args.iter().any(expr_refs_ve), ExprKind::Yield { key, value } => { key.as_deref().is_some_and(expr_refs_ve) diff --git a/src/web_prelude/usage.rs b/src/web_prelude/usage.rs index 0aff20e427..d7e0fd8be4 100644 --- a/src/web_prelude/usage.rs +++ b/src/web_prelude/usage.rs @@ -505,5 +505,6 @@ fn scan_expr(expr: &Expr, usage: &mut Usage) { | ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } | ExprKind::MagicConstant(_) => {} + ExprKind::ObjectClassName { object } => scan_expr(object, usage), } } diff --git a/tests/codegen/static_class_features.rs b/tests/codegen/static_class_features.rs index 7d39d90df8..5bc30e5f8e 100644 --- a/tests/codegen/static_class_features.rs +++ b/tests/codegen/static_class_features.rs @@ -65,6 +65,42 @@ fn test_class_class_concat_in_message() { assert_eq!(out, "From: Logger"); } +/// Verifies `$object::class` returns the object's fully-qualified runtime class name. +#[test] +fn test_object_class_name_returns_runtime_fqn() { + let out = compile_and_run( + " { + assert_eq!(object.kind, ExprKind::Variable("pippo".to_string())); + } + other => panic!("expected ObjectClassName, got {:?}", other), + } +} + +/// Verifies that `::class` accepts a call expression receiver without duplicating it in the AST. +#[test] +fn test_parse_call_expression_object_class_name() { + let stmts = parse_source(" match &object.kind { + ExprKind::FunctionCall { name, args } => { + assert_eq!(name.as_str(), "make"); + assert!(args.is_empty()); + } + other => panic!("expected FunctionCall receiver, got {:?}", other), + }, + other => panic!("expected ObjectClassName, got {:?}", other), + } +} + // --- new self() / new static() / new parent() ---