feat(parser): add validation for PHP 8.5 closure usage in constants and defaults

- Add php85 flag to control closure validation behavior based on PHP version
- Implement rejectUnsupportedClosure method to detect and reject closures in constant expressions
- Add validation for closures in class constant declarations with appropriate error messages
- Add validation for closures in property default values with appropriate error messages
- Add validation for closures in parameter default values with appropriate error messages
- Update documentation to reflect PHP 8.5 closure incompatibility in constants and defaults
- Add comprehensive test cases covering various closure usage scenarios in PHP 8.5
- Remove previously allowed PHP 8.5 class constant closure test case from validation tests
master
韩天峰 1 day ago
parent c5f478766e
commit 6d45c64b4d
  1. 1
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 1
      docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md
  3. 39
      phpunit/src/ConstantExpressionValidatorTest.php
  4. 39
      src/Transform/ConstantExpressionValidationVisitor.php

@ -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`

@ -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. |

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

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

Loading…
Cancel
Save