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]
master
韩天峰 2 days ago
parent f64a1d5cb7
commit c5f478766e
  1. 1
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 6
      src/CompilerBase.php
  3. 2
      src/Generator/AnonClassGenerator.php
  4. 7
      src/Parser/ConstantExpressionTrait.php
  5. 14
      src/Transform/RuntimeAttributeFactoryLowering.php
  6. 66
      src/Transform/Visitor.php
  7. 2
      src/Translator.php
  8. 48
      tests/compiler/namespace/magic-namespace.phpt
  9. 151
      tests/compiler/namespace/relative-name.phpt
  10. 69
      tests/compiler/object_property/property-magic-constant.phpt

@ -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 执行同等作用域检查。

@ -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);
}
}

@ -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");
}

@ -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':

@ -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());

@ -18,6 +18,14 @@ use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
class Visitor extends NodeVisitorAbstract
{
private string $namespaceMagicName = '';
private string $propertyMagicName = '';
/** @var list<array{string, int}> */
private array $propertyMagicContextStack = [];
private int $propertyMagicBoundaryDepth = 0;
/** @var array<int, true> */
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,

@ -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");
}

@ -0,0 +1,48 @@
--TEST--
__NAMESPACE__ resolves in namespace, class, and global scopes
--FILE--
<?php
namespace Project\Feature {
#[\Attribute(\Attribute::TARGET_CLASS)]
class NamespaceName
{
public function __construct(public string $name)
{
}
}
const CURRENT_NAMESPACE = __NAMESPACE__;
function namespaceName(): string
{
return __NAMESPACE__;
}
#[NamespaceName(__NAMESPACE__)]
class Scope
{
public function namespaceName(): string
{
return __NAMESPACE__;
}
}
}
namespace {
function main(): void
{
var_dump(__NAMESPACE__);
var_dump(Project\Feature\namespaceName());
var_dump((new Project\Feature\Scope())->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"

@ -0,0 +1,151 @@
--TEST--
namespace\name resolves relative functions, constants, classes, members, and declarations
--FILE--
<?php
namespace {
const GLOBAL_VALUE = 'global constant';
function globalHelper(): string
{
return 'global function';
}
class GlobalTarget
{
public const VALUE = 'global class';
}
}
namespace RelativeNames {
const VALUE = 'namespaced constant';
#[\Attribute(\Attribute::TARGET_CLASS)]
class Marker
{
public function __construct(public string $value)
{
}
}
function helper(): string
{
return 'namespaced function';
}
interface Contract
{
public function target(): namespace\Target;
}
trait Feature
{
public function feature(): string
{
return namespace\helper();
}
}
class Base
{
public function base(): string
{
return 'anonymous base';
}
}
class Target
{
public const VALUE = 'class constant';
public static string $value = 'static property';
public static function method(): string
{
return 'static method';
}
}
class Failure extends \Exception
{
}
#[namespace\Marker(namespace\VALUE)]
class Child extends namespace\Base implements namespace\Contract
{
use namespace\Feature;
public function target(): namespace\Target
{
return new namespace\Target();
}
public function accepts(namespace\Target $target): bool
{
return $target instanceof namespace\Target;
}
}
function run(): void
{
var_dump(namespace\VALUE);
var_dump(namespace\helper());
$callable = namespace\helper(...);
var_dump($callable());
var_dump(namespace\Target::VALUE);
var_dump(namespace\Target::$value);
var_dump(namespace\Target::method());
$child = new namespace\Child();
$target = $child->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"

@ -0,0 +1,69 @@
--TEST--
PHP 8.4 __PROPERTY__ resolves in property contexts only
--FILE--
<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class PropertyName
{
public function __construct(public string $name)
{
}
}
class PropertyMagicConstants
{
public string $first = __PROPERTY__, $second = __PROPERTY__;
#[PropertyName(__PROPERTY__)]
public string $annotated = __PROPERTY__;
public string $hooked {
get => __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) ""
Loading…
Cancel
Save