diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index 9eec1496..380f3975 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -19,6 +19,7 @@ - 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 执行同等作用域检查。 - 不支持闭包或箭头函数按引用返回。 +- 暂不支持 PHP 8.5 在全局常量、类常量、参数默认值或属性默认值中使用 `static function`;初始化表达式内嵌套的闭包同样会在编译期被拒绝。 - `__construct()` 不允许返回值。 - 参数默认值不允许出现在必填参数之前(`PHP`允许,但会直接丢弃此默认参数)。 - 不支持引用可变参数 `&...$args`。 diff --git a/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md b/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md index 0eb378bb..86bad89d 100644 --- a/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md +++ b/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md @@ -83,6 +83,7 @@ These items should be documented with the exact boundary. |---|---|---| | Variable variables (`$$var`) | Pending | Add a function-local symbol table mirror for dynamic locals, and disable or synchronize native locals that escape into dynamic lookup. | | Closure or arrow function returning by reference | Pending | Closure metadata and wrappers must preserve return-by-reference and emit `ReturnRef`. | +| PHP 8.5 closures in constants, parameter defaults or property defaults | Pending | Use context-aware runtime initializers: cache constants and property defaults per request, create parameter defaults per omitted call, and never place request-local zvals in persistent MINIT storage. | | Closure and arrow function by-reference parameters | Pending | Closure arginfo must preserve by-reference parameters and call lowering must pass reference slots. | | By-reference variadic parameters (`&...$args`) | Pending | Variadic storage must preserve references instead of copying values. | | By-reference parameters with default values | Pending | Need PHP-compatible handling for omitted arguments using temporary default values while still binding references for passed arguments. | diff --git a/phpunit/src/ConstantExpressionValidatorTest.php b/phpunit/src/ConstantExpressionValidatorTest.php index cb7ec3fb..4e4a3380 100644 --- a/phpunit/src/ConstantExpressionValidatorTest.php +++ b/phpunit/src/ConstantExpressionValidatorTest.php @@ -177,10 +177,6 @@ final class ConstantExpressionValidatorTest extends PHPUnit\Framework\TestCase 'function f(int $seed) { static $value = loadValue($seed); }', '8.4', ]; - yield 'PHP 8.5 class constant allows static closure' => [ - 'class C { const VALUE = static function (): int { return 1; }; }', - '8.5', - ]; yield 'PHP 8.5 property allows scalar cast' => ['class C { public int $value = (int) 1.5; }', '8.5']; } @@ -232,6 +228,41 @@ final class ConstantExpressionValidatorTest extends PHPUnit\Framework\TestCase '8.4', 'Constant expression contains invalid operations', ]; + yield 'PHP 8.5 global constant closure is not supported by TypePHP' => [ + 'const VALUE = static function (): int { return 1; };', + '8.5', + 'Closures in constant declarations are not supported by TypePHP', + ]; + yield 'PHP 8.5 class constant closure is not supported by TypePHP' => [ + 'class C { const VALUE = static function (): int { return 1; }; }', + '8.5', + 'Closures in constant declarations are not supported by TypePHP', + ]; + yield 'PHP 8.5 nested class constant closure is not supported by TypePHP' => [ + 'class C { const VALUE = [static function (): int { return 1; }]; }', + '8.5', + 'Closures in constant declarations are not supported by TypePHP', + ]; + yield 'PHP 8.5 parameter default closure is not supported by TypePHP' => [ + 'function f(Closure $value = static function (): int { return 1; }) {}', + '8.5', + 'Closures in parameter default values are not supported by TypePHP', + ]; + yield 'PHP 8.5 property default closure is not supported by TypePHP' => [ + 'class C { public Closure $value = static function (): int { return 1; }; }', + '8.5', + 'Closures in property default values are not supported by TypePHP', + ]; + yield 'PHP 8.5 nested parameter default closure is not supported by TypePHP' => [ + 'function f(mixed $value = [static function (): int { return 1; }]) {}', + '8.5', + 'Closures in parameter default values are not supported by TypePHP', + ]; + yield 'PHP 8.5 nested property default closure is not supported by TypePHP' => [ + 'class C { public mixed $value = [static function (): int { return 1; }]; }', + '8.5', + 'Closures in property default values are not supported by TypePHP', + ]; } public function testExposesPhpStyleNodeWhitelist(): void diff --git a/src/Transform/ConstantExpressionValidationVisitor.php b/src/Transform/ConstantExpressionValidationVisitor.php index e8803a15..157323ac 100644 --- a/src/Transform/ConstantExpressionValidationVisitor.php +++ b/src/Transform/ConstantExpressionValidationVisitor.php @@ -10,6 +10,8 @@ namespace TypePhp\Transform; use Closure; use PhpParser\Node; +use PhpParser\Node\Expr; +use PhpParser\NodeFinder; use PhpParser\NodeVisitorAbstract; use TypePhp\Exception\SyntaxError; @@ -25,6 +27,7 @@ use TypePhp\Exception\SyntaxError; final class ConstantExpressionValidationVisitor extends NodeVisitorAbstract { private readonly ConstantExpressionValidator $validator; + private readonly bool $php85; /** @param null|Closure(Node, string): never $fatalError */ public function __construct( @@ -33,6 +36,7 @@ final class ConstantExpressionValidationVisitor extends NodeVisitorAbstract ) { $this->validator = new ConstantExpressionValidator($phpVersion); + $this->php85 = version_compare($phpVersion, '8.5', '>='); } public function enterNode(Node $node): null @@ -60,6 +64,10 @@ final class ConstantExpressionValidationVisitor extends NodeVisitorAbstract if ($node instanceof Node\Stmt\ClassConst) { foreach ($node->consts as $constant) { + $this->rejectUnsupportedClosure( + $constant->value, + 'Closures in constant declarations are not supported by TypePHP', + ); $this->validator->validate($constant->value, allowDynamic: false); } return null; @@ -68,6 +76,10 @@ final class ConstantExpressionValidationVisitor extends NodeVisitorAbstract if ($node instanceof Node\Stmt\Property) { foreach ($node->props as $property) { if ($property->default !== null) { + $this->rejectUnsupportedClosure( + $property->default, + 'Closures in property default values are not supported by TypePHP', + ); $this->validator->validate($property->default, allowDynamic: false); } } @@ -80,12 +92,20 @@ final class ConstantExpressionValidationVisitor extends NodeVisitorAbstract } if ($node instanceof Node\Param && $node->default !== null) { + $this->rejectUnsupportedClosure( + $node->default, + 'Closures in parameter default values are not supported by TypePHP', + ); $this->validator->validate($node->default, allowDynamic: true); return null; } if ($node instanceof Node\Stmt\Const_) { foreach ($node->consts as $constant) { + $this->rejectUnsupportedClosure( + $constant->value, + 'Closures in constant declarations are not supported by TypePHP', + ); $this->validator->validate($constant->value, allowDynamic: true); } return null; @@ -93,4 +113,23 @@ final class ConstantExpressionValidationVisitor extends NodeVisitorAbstract return null; } + + private function rejectUnsupportedClosure(Expr $expression, string $message): void + { + if (!$this->php85) { + return; + } + + $closure = (new NodeFinder())->findFirstInstanceOf($expression, Expr\Closure::class); + if ($closure === null) { + return; + } + + // TODO(PHP 8.5): Lower closures in constant-expression declaration + // values to context-aware runtime initializer plans. Constants and + // properties cache a Closure per request, while parameter defaults + // create one per omitted call. Never store a request-local zval in + // persistent MINIT class metadata. + throw new SyntaxError($message); + } }