From c5f478766e55b9f26db706f3e327f1782a84c13b Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 24 Aug 2026 14:34:27 +0800 Subject: [PATCH] feat(compiler): add support for PHP 8.5 magic constants and namespace relative names - Add support for __NAMESPACE__ magic constant resolution in namespace, class, and global scopes - Add support for __PROPERTY__ magic constant that resolves to property name within property context - Implement namespace\name relative name resolution for functions, constants, classes, and members - Update AnonClassGenerator to properly parse identifiers instead of using toString() - Extend CompilerBase to handle Scalar_MagicConst_Namespace and Scalar_MagicConst_Property - Modify class inheritance and interface implementation to use parseIdentifier for proper name resolution - Add comprehensive tests for namespace magic constant behavior across different scopes - Add tests for property magic constant resolving correctly in property contexts only - Implement runtime attribute factory lowering with proper namespace context handling - Update visitor pattern to track property magic boundaries and namespace context correctly - Document incompatibility with PHP 8.5 #[NoDiscard] --- docs/INCOMPATIBLE_PHP_FEATURES.md | 1 + src/CompilerBase.php | 6 +- src/Generator/AnonClassGenerator.php | 2 +- src/Parser/ConstantExpressionTrait.php | 7 + .../RuntimeAttributeFactoryLowering.php | 14 +- src/Transform/Visitor.php | 66 +++++++- src/Translator.php | 2 +- tests/compiler/namespace/magic-namespace.phpt | 48 ++++++ tests/compiler/namespace/relative-name.phpt | 151 ++++++++++++++++++ .../property-magic-constant.phpt | 69 ++++++++ 10 files changed, 360 insertions(+), 6 deletions(-) create mode 100644 tests/compiler/namespace/magic-namespace.phpt create mode 100644 tests/compiler/namespace/relative-name.phpt create mode 100644 tests/compiler/object_property/property-magic-constant.phpt diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index d3b182f3..9eec1496 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -14,6 +14,7 @@ ## 声明与类型 - 不支持可变变量 `$$var`。 +- 不支持 PHP 8.5 `#[NoDiscard]` 及用于显式丢弃返回值的 `(void)` 语法。 - PHP 8.4 property hooks 会编译为 AOT getter/setter,并注册对应的 Zend hook 元数据;直接属性读写、Reflection 和对象遍历均受支持。当前不支持对 hook 属性取引用。 - PHP 8.4 Reflection Lazy Object 不能用于 TypePHP AOT 类。AOT 类以 persistent internal class 注册,而 Zend 的 `zend_object_make_lazy()` 明确拒绝 internal class;运行时动态加载的 ZendPHP user class 不受此限制。 - 支持 `private(set)` 与 `protected(set)` 非对称属性可见性,并通过 PHP 8.4+ 的类级对象 handler 执行同等作用域检查。 diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 1ac7f145..c116e11c 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -893,6 +893,8 @@ class CompilerBase implements PropertyAccessContext case 'Scalar_MagicConst_Method': case 'Scalar_MagicConst_Class': case 'Scalar_MagicConst_Trait': + case 'Scalar_MagicConst_Namespace': + case 'Scalar_MagicConst_Property': return $this->parseMagicConst($expr); case 'Scalar_InterpolatedString': return $this->parseInterpolatedString($expr); @@ -3788,12 +3790,12 @@ class CompilerBase implements PropertyAccessContext $classDef->name = new Node\Identifier($className); // 继承父类和接口可能是 use 的名称,需要转换成全限定名称 if ($classDef->extends !== null) { - $parentClass = $this->getNamespacedClassName($classDef->extends->toString()); + $parentClass = $this->getNamespacedClassName($this->parseIdentifier($classDef->extends)); $classDef->extends = new Node\Name\FullyQualified($parentClass); } if (!empty($classDef->implements)) { foreach ($classDef->implements as $i => $iface) { - $ifaceName = $this->getNamespacedClassName($iface->toString()); + $ifaceName = $this->getNamespacedClassName($this->parseIdentifier($iface)); $classDef->implements[$i] = new Node\Name\FullyQualified($ifaceName); } } diff --git a/src/Generator/AnonClassGenerator.php b/src/Generator/AnonClassGenerator.php index c98eb024..f3d21193 100644 --- a/src/Generator/AnonClassGenerator.php +++ b/src/Generator/AnonClassGenerator.php @@ -49,7 +49,7 @@ trait AnonClassGenerator continue; } foreach ($stmt->traits as $traitName) { - $fullName = $this->getNamespacedClassName($traitName->toString()); + $fullName = $this->getNamespacedClassName($this->parseIdentifier($traitName)); if (!$this->hasClass($fullName)) { $this->fatalError($stmt, "Trait `{$fullName}` not found"); } diff --git a/src/Parser/ConstantExpressionTrait.php b/src/Parser/ConstantExpressionTrait.php index 3d71265d..3515fbb9 100644 --- a/src/Parser/ConstantExpressionTrait.php +++ b/src/Parser/ConstantExpressionTrait.php @@ -147,6 +147,13 @@ trait ConstantExpressionTrait return '"' . $this->escapeString($this->file) . '"'; case 'Scalar_MagicConst_Line': return (string) $expr->getStartLine(); + case 'Scalar_MagicConst_Namespace': + return '"' . $this->escapeString($this->namespace) . '"'; + case 'Scalar_MagicConst_Property': + // Visitor normally folds this constant before property hooks + // are lowered to generated methods. Keep the fallback aligned + // with PHP, where it is an empty string outside a property. + return '""'; case 'Scalar_MagicConst_Function': return '"' . $this->escapeString($function) . '"'; case 'Scalar_MagicConst_Class': diff --git a/src/Transform/RuntimeAttributeFactoryLowering.php b/src/Transform/RuntimeAttributeFactoryLowering.php index aac35aa5..5dbc5181 100644 --- a/src/Transform/RuntimeAttributeFactoryLowering.php +++ b/src/Transform/RuntimeAttributeFactoryLowering.php @@ -178,10 +178,15 @@ final class RuntimeAttributeFactoryLowering extends NodeVisitorAbstract $context = $this->classStack === [] ? ['namespace' => '', 'parent' => ''] : $this->classStack[array_key_last($this->classStack)]; - $traverser->addVisitor(new class($context['namespace'], $context['parent']) extends NodeVisitorAbstract { + $traverser->addVisitor(new class( + $context['namespace'], + $context['parent'], + $this->namespace, + ) extends NodeVisitorAbstract { public function __construct( private readonly string $class, private readonly string $parent, + private readonly string $namespace, ) { } @@ -197,6 +202,13 @@ final class RuntimeAttributeFactoryLowering extends NodeVisitorAbstract } } if ($node instanceof Node\Name) { + // Attribute factories are created while the outer + // traverser is entering the Attribute node, before its + // argument names have been visited by NameResolver. + if ($node instanceof Node\Name\Relative) { + $name = ltrim($this->namespace . '\\' . $node->toString(), '\\'); + return new Node\Name\FullyQualified($name, $node->getAttributes()); + } $resolved = $node->getAttribute('resolvedName'); if ($resolved instanceof Node\Name) { return new Node\Name\FullyQualified($resolved->toString(), $resolved->getAttributes()); diff --git a/src/Transform/Visitor.php b/src/Transform/Visitor.php index 0ac5d414..1abe1e40 100644 --- a/src/Transform/Visitor.php +++ b/src/Transform/Visitor.php @@ -18,6 +18,14 @@ use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic; class Visitor extends NodeVisitorAbstract { + private string $namespaceMagicName = ''; + private string $propertyMagicName = ''; + /** @var list */ + private array $propertyMagicContextStack = []; + private int $propertyMagicBoundaryDepth = 0; + /** @var array */ + private array $propertyMagicBoundaries = []; + /** @param null|Closure(Node, string): void $warning */ public function __construct( private readonly ?Closure $warning = null, @@ -25,8 +33,51 @@ class Visitor extends NodeVisitorAbstract ) { } - public function enterNode(Node $node): null + public function enterNode(Node $node): null|Node { + if ($node instanceof Stmt\Namespace_) { + $this->namespaceMagicName = $node->name?->toString() ?? ''; + } + if ($node instanceof Node\Name\Relative) { + // namespace\name is bound to the current namespace and never + // participates in imports or global function/constant fallback. + $resolved = $node->getAttribute('resolvedName'); + $name = $resolved instanceof Node\Name + ? $resolved->toString() + : ltrim($this->namespaceMagicName . '\\' . $node->toString(), '\\'); + return new Node\Name\FullyQualified($name, $node->getAttributes()); + } + + if ($node instanceof Stmt\Property) { + $this->propertyMagicContextStack[] = [ + $this->propertyMagicName, + $this->propertyMagicBoundaryDepth, + ]; + $this->propertyMagicName = $node->props[0]->name->toString(); + $this->propertyMagicBoundaryDepth = 0; + } elseif ($node instanceof Node\PropertyItem) { + // Multi-property declarations resolve __PROPERTY__ separately for + // every initializer. Attributes on the declaration use the first + // property, matching ZendPHP. + $this->propertyMagicName = $node->name->toString(); + } elseif ($this->propertyMagicName !== '' + && (($node instanceof Node\FunctionLike && !$node instanceof Node\PropertyHook) + || $node instanceof Stmt\ClassLike) + ) { + ++$this->propertyMagicBoundaryDepth; + $this->propertyMagicBoundaries[spl_object_id($node)] = true; + } + + if ($node instanceof Node\Scalar\MagicConst\Property) { + $value = $this->propertyMagicBoundaryDepth === 0 + ? $this->propertyMagicName + : ''; + return new Node\Scalar\String_($value, $node->getAttributes()); + } + if ($node instanceof Node\Scalar\MagicConst\Namespace_) { + return new Node\Scalar\String_($this->namespaceMagicName, $node->getAttributes()); + } + $this->guard($node, static fn () => CompileTimeAttribute::validateNode($node)); $this->guard($node, static fn () => NativeClassAttributeLowering::lower($node), 'Native'); $this->guard($node, static fn () => FunctionAttributeLowering::lower($node)); @@ -38,6 +89,19 @@ class Visitor extends NodeVisitorAbstract public function leaveNode(Node $node): null { + $nodeId = spl_object_id($node); + if (isset($this->propertyMagicBoundaries[$nodeId])) { + unset($this->propertyMagicBoundaries[$nodeId]); + --$this->propertyMagicBoundaryDepth; + } + if ($node instanceof Stmt\Property) { + [$this->propertyMagicName, $this->propertyMagicBoundaryDepth] + = array_pop($this->propertyMagicContextStack); + } + if ($node instanceof Stmt\Namespace_) { + $this->namespaceMagicName = ''; + } + if ($node instanceof Stmt\Function_ || $node instanceof Stmt\ClassMethod || $node instanceof Node\Expr\Closure) { $this->guard( $node, diff --git a/src/Translator.php b/src/Translator.php index 1714cebc..e496511a 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2985,7 +2985,7 @@ CODE; } foreach ($classStmt->traits as $trait1) { - $traitFullName = $this->getNamespacedClassName($trait1->toString()); + $traitFullName = $this->getNamespacedClassName($this->parseIdentifier($trait1)); if (!$this->hasClass($traitFullName)) { $this->fatalError($classStmt, "Trait `{$traitFullName}` not found"); } diff --git a/tests/compiler/namespace/magic-namespace.phpt b/tests/compiler/namespace/magic-namespace.phpt new file mode 100644 index 00000000..3e1981b5 --- /dev/null +++ b/tests/compiler/namespace/magic-namespace.phpt @@ -0,0 +1,48 @@ +--TEST-- +__NAMESPACE__ resolves in namespace, class, and global scopes +--FILE-- +namespaceName()); + var_dump(Project\Feature\CURRENT_NAMESPACE); + $class = new ReflectionClass(Project\Feature\Scope::class); + var_dump($class->getAttributes(Project\Feature\NamespaceName::class)[0]->getArguments()[0]); + } +} +?> +--EXPECT-- +string(0) "" +string(15) "Project\Feature" +string(15) "Project\Feature" +string(15) "Project\Feature" +string(15) "Project\Feature" diff --git a/tests/compiler/namespace/relative-name.phpt b/tests/compiler/namespace/relative-name.phpt new file mode 100644 index 00000000..bd623d0c --- /dev/null +++ b/tests/compiler/namespace/relative-name.phpt @@ -0,0 +1,151 @@ +--TEST-- +namespace\name resolves relative functions, constants, classes, members, and declarations +--FILE-- +target(); + var_dump($child->accepts($target)); + var_dump($child->feature()); + $attribute = (new \ReflectionClass(namespace\Child::class)) + ->getAttributes(namespace\Marker::class)[0]; + var_dump($attribute->getArguments()[0]); + + $anonymous = new class extends namespace\Base implements namespace\Contract { + use namespace\Feature; + + public function target(): namespace\Target + { + return new namespace\Target(); + } + }; + var_dump($anonymous->base()); + var_dump($anonymous->feature()); + var_dump($anonymous->target() instanceof namespace\Target); + + try { + throw new namespace\Failure('caught'); + } catch (namespace\Failure $exception) { + var_dump($exception->getMessage()); + } + } +} + +namespace { + function main(): void + { + var_dump(namespace\GLOBAL_VALUE); + var_dump(namespace\globalHelper()); + var_dump(namespace\GlobalTarget::VALUE); + RelativeNames\run(); + } +} +?> +--EXPECT-- +string(15) "global constant" +string(15) "global function" +string(12) "global class" +string(19) "namespaced constant" +string(19) "namespaced function" +string(19) "namespaced function" +string(14) "class constant" +string(15) "static property" +string(13) "static method" +bool(true) +string(19) "namespaced function" +string(19) "namespaced constant" +string(14) "anonymous base" +string(19) "namespaced function" +bool(true) +string(6) "caught" diff --git a/tests/compiler/object_property/property-magic-constant.phpt b/tests/compiler/object_property/property-magic-constant.phpt new file mode 100644 index 00000000..f15f314c --- /dev/null +++ b/tests/compiler/object_property/property-magic-constant.phpt @@ -0,0 +1,69 @@ +--TEST-- +PHP 8.4 __PROPERTY__ resolves in property contexts only +--FILE-- + __PROPERTY__; + set { + var_dump(__PROPERTY__); + } + } + + private string $nested { + get => (function (): string { + return __PROPERTY__; + })(); + } + + public function outsideProperty(): string + { + return __PROPERTY__; + } + + public function nestedProperty(): string + { + return $this->nested; + } +} + +function main(): void +{ + $object = new PropertyMagicConstants(); + var_dump($object->first); + var_dump($object->second); + var_dump($object->annotated); + $property = new ReflectionProperty(PropertyMagicConstants::class, 'annotated'); + var_dump($property->getAttributes(PropertyName::class)[0]->getArguments()[0]); + var_dump($object->hooked); + $object->hooked = 'ignored'; + var_dump($object->outsideProperty()); + var_dump($object->nestedProperty()); + var_dump(__PROPERTY__); +} +?> +--EXPECT-- +string(5) "first" +string(6) "second" +string(9) "annotated" +string(9) "annotated" +string(6) "hooked" +string(6) "hooked" +string(0) "" +string(0) "" +string(0) ""