From 44095252d21a45b6cbecc60460d64730d945bda3 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 24 Aug 2026 20:27:17 +0800 Subject: [PATCH] feat(parser): add support for final promoted properties and exit named arguments - Implement final promoted property parsing with explicit visibility requirement - Add support for exit(message: $value) named argument syntax - Reject unknown named arguments in exit/die function calls - Enforce explicit visibility declaration for final promoted properties - Add test coverage for final promoted property inheritance errors - Update documentation for supported PHP 8.4+ features and limitations - Generate proper reflection metadata for final promoted properties - Support native class final promoted property compilation --- docs/INCOMPATIBLE_PHP_FEATURES.md | 2 + phpunit/code/exit-unknown-named-argument.php | 6 +++ ...l-promoted-property-without-visibility.php | 9 ++++ ...eritance_error_final_promoted_property.php | 14 ++++++ phpunit/src/ClassTest.php | 8 ++++ phpunit/src/FunctionTest.php | 5 +++ phpunit/src/InheritanceErrorTest.php | 8 ++++ src/Parser/FunctionCallTrait.php | 27 ++++++++++++ src/Preprocessor.php | 9 ++++ tests/compiler/basic/exit-named-message.phpt | 15 +++++++ .../object_ctor/final-promoted-property.phpt | 44 +++++++++++++++++++ 11 files changed, 147 insertions(+) create mode 100644 phpunit/code/exit-unknown-named-argument.php create mode 100644 phpunit/code/final-promoted-property-without-visibility.php create mode 100644 phpunit/code/inheritance_error_final_promoted_property.php create mode 100644 tests/compiler/basic/exit-named-message.phpt create mode 100644 tests/compiler/object_ctor/final-promoted-property.phpt diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index 0427f653..936cf9a1 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -20,6 +20,7 @@ - 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)` 非对称属性可见性,包括 constructor property promotion;Zend-backed 对象通过 PHP 8.4+ 类级 object handler 执行作用域检查,并保留 promoted/set visibility/implicit final 反射标志;Native 对象通过编译期访问检查执行同等作用域规则。 +- 支持 final constructor property promotion,但 TypePHP 要求同时显式声明 `public`、`protected` 或 `private`;不接受 PHP 8.5 的 `final int $value` 隐式 public promotion 写法。该语法作为 TypePHP 扩展不受所链接 `libphp` 的源码语法版本限制,使用 PHP 8.4 `libphp.so` 时仍然可用。 - 不支持闭包或箭头函数按引用返回。 - 暂不支持 PHP 8.5 在全局常量、类常量、参数默认值或属性默认值中使用 `static function`;初始化表达式内嵌套的闭包同样会在编译期被拒绝。 - `__construct()` 不允许返回值。 @@ -37,6 +38,7 @@ ## 调用与引用 +- `exit(message: $value)` 可作为 TypePHP named-argument 扩展使用;它与位置参数 `exit($value)` 进入同一退出路径。 - TypePHP 使用严格参数数量规则:非 variadic 函数不接受声明范围之外的额外参数;`func_get_args()` 不会隐式放宽签名。 - 已知签名的普通函数、普通方法和 native 直调支持引用参数及写回;不要把编译器内部跨 Trait 动态分派的限制误写成“TypePHP 不支持引用参数”。 - 闭包和箭头函数不支持引用参数。 diff --git a/phpunit/code/exit-unknown-named-argument.php b/phpunit/code/exit-unknown-named-argument.php new file mode 100644 index 00000000..9aae2017 --- /dev/null +++ b/phpunit/code/exit-unknown-named-argument.php @@ -0,0 +1,6 @@ +exec( + 'Final promoted property must explicitly declare public, protected, or private visibility', + 'final-promoted-property-without-visibility.php', + ); + } + public function testTraitMayCallProtectedParentMethod() { // A protected parent method is reachable via parent:: from a trait, diff --git a/phpunit/src/FunctionTest.php b/phpunit/src/FunctionTest.php index 07dfecac..3447c0d1 100644 --- a/phpunit/src/FunctionTest.php +++ b/phpunit/src/FunctionTest.php @@ -88,6 +88,11 @@ class FunctionTest extends \BaseTest $this->exec('Unknown named argument `foo`', 'internal-call-unknown-named-arg.php'); } + public function testExitRejectsUnknownNamedArgument(): void + { + $this->exec('Unknown named argument `text`', 'exit-unknown-named-argument.php'); + } + public function testInternalCallMissingRequiredNamedArgument() { $this->exec('Named argument `replace` is missing default value', 'internal-call-missing-required-named-arg.php'); diff --git a/phpunit/src/InheritanceErrorTest.php b/phpunit/src/InheritanceErrorTest.php index de5ef54f..3cd9bc9b 100644 --- a/phpunit/src/InheritanceErrorTest.php +++ b/phpunit/src/InheritanceErrorTest.php @@ -196,6 +196,14 @@ class InheritanceErrorTest extends TestCase ); } + public function testCannotOverrideFinalPromotedProperty(): void + { + $this->exec( + 'Cannot override final property FinalPromotedPropertyParent::$value', + 'inheritance_error_final_promoted_property.php', + ); + } + public function testInterfaceMethodStaticMismatch() { $this->exec('must be compatible', 'interface_method_static_mismatch.php'); diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index d70ffc4e..c3ef736d 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -101,6 +101,10 @@ trait FunctionCallTrait } elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') { $name = $this->parseIdentifier($expr->name); $globalName = ltrim($name, '\\'); + $namedExit = $this->parseNamedExitMessageCall($globalName, $expr); + if ($namedExit !== null) { + return $namedExit; + } if ($globalName === 'clone' && !$expr->isFirstClassCallable() && $this->class) { // PHP 8.5 clone-with applies property updates in the lexical // scope of the call site. Direct AOT method calls do not leave @@ -224,4 +228,27 @@ trait FunctionCallTrait return $this->genPlaceHolder($placeHolder); } } + + private function parseNamedExitMessageCall(string $name, Expr\FuncCall $expr): ?string + { + if (!in_array(strtolower($name), ['exit', 'die'], true) + || $expr->isFirstClassCallable() + || count($expr->args) !== 1 + ) { + return null; + } + + $arg = $expr->args[0]; + if (!$arg instanceof Node\Arg + || $arg->unpack + || $arg->name?->toString() !== 'message' + ) { + return null; + } + + // PHP 8.4 exposes this builtin argument as $status. TypePHP also + // accepts the clearer $message alias and lowers it to the same AOT + // exit path without changing php-parser's call representation. + return $this->parseExit(new Expr\Exit_($arg->value, $expr->getAttributes())); + } } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index de5cf3f0..59cff735 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -1411,6 +1411,15 @@ class Preprocessor extends CompilerBase */ protected function addClassProperty(string $name, int $flags, ?NodeAbstract $typeNode, $defaultNode, bool $nullable, NodeAbstract $errorNode, bool $promoted = false): PropertyDef { + if ($promoted + && ($flags & Modifiers::FINAL) + && !($flags & Modifiers::VISIBILITY_MASK) + ) { + $this->fatalError( + $errorNode, + 'Final promoted property must explicitly declare public, protected, or private visibility', + ); + } $flags = $this->parseModifiers($flags); $this->validateAsymmetricPropertyDeclaration($name, $flags, $typeNode, $errorNode); [$type, $class] = $this->resolveTypeDecl($typeNode, self::DECL_TYPE_OF_PROPERTY); diff --git a/tests/compiler/basic/exit-named-message.phpt b/tests/compiler/basic/exit-named-message.phpt new file mode 100644 index 00000000..91bfd6f1 --- /dev/null +++ b/tests/compiler/basic/exit-named-message.phpt @@ -0,0 +1,15 @@ +--TEST-- +exit() accepts the TypePHP message named argument +--ENV-- +USE_ZEND_ALLOC=0 +--FILE-- + +--EXPECT-- +named exit diff --git a/tests/compiler/object_ctor/final-promoted-property.phpt b/tests/compiler/object_ctor/final-promoted-property.phpt new file mode 100644 index 00000000..277e99ad --- /dev/null +++ b/tests/compiler/object_ctor/final-promoted-property.phpt @@ -0,0 +1,44 @@ +--TEST-- +TypePHP supports explicit-visibility final promoted properties with PHP 8.4 libphp +--FILE-- +value); + + $property = new ReflectionProperty(FinalPromotedProperty::class, 'value'); + var_dump( + $property->isPublic(), + $property->isPromoted(), + $property->isFinal(), + ); + + $native = new NativeFinalPromotedProperty(42); + var_dump($native->value); +} +?> +--EXPECT-- +string(8) "promoted" +bool(true) +bool(true) +bool(true) +int(42)