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
master
韩天峰 1 day ago
parent 9f1cf07511
commit 44095252d2
  1. 2
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 6
      phpunit/code/exit-unknown-named-argument.php
  3. 9
      phpunit/code/final-promoted-property-without-visibility.php
  4. 14
      phpunit/code/inheritance_error_final_promoted_property.php
  5. 8
      phpunit/src/ClassTest.php
  6. 5
      phpunit/src/FunctionTest.php
  7. 8
      phpunit/src/InheritanceErrorTest.php
  8. 27
      src/Parser/FunctionCallTrait.php
  9. 9
      src/Preprocessor.php
  10. 15
      tests/compiler/basic/exit-named-message.phpt
  11. 44
      tests/compiler/object_ctor/final-promoted-property.phpt

@ -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 不支持引用参数”。
- 闭包和箭头函数不支持引用参数。

@ -0,0 +1,6 @@
<?php
function main(): void
{
exit(text: 'unsupported');
}

@ -0,0 +1,9 @@
<?php
class FinalPromotedPropertyWithoutVisibility
{
public function __construct(
final int $value,
) {
}
}

@ -0,0 +1,14 @@
<?php
class FinalPromotedPropertyParent
{
public function __construct(
public final string $value,
) {
}
}
class FinalPromotedPropertyChild extends FinalPromotedPropertyParent
{
public string $value = 'child';
}

@ -679,6 +679,14 @@ class ClassTest extends \BaseTest
);
}
public function testFinalPromotedPropertyRequiresExplicitVisibility(): void
{
$this->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,

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

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

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

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

@ -0,0 +1,15 @@
--TEST--
exit() accepts the TypePHP message named argument
--ENV--
USE_ZEND_ALLOC=0
--FILE--
<?php
function main(): void
{
exit(message: "named exit\n");
echo "unreachable\n";
}
?>
--EXPECT--
named exit

@ -0,0 +1,44 @@
--TEST--
TypePHP supports explicit-visibility final promoted properties with PHP 8.4 libphp
--FILE--
<?php
class FinalPromotedProperty
{
public function __construct(
public final string $value,
) {
}
}
#[Native]
class NativeFinalPromotedProperty
{
public function __construct(
public final int $value,
) {
}
}
function main(): void
{
$object = new FinalPromotedProperty('promoted');
var_dump($object->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)
Loading…
Cancel
Save