- Implement ConstantExpressionValidationVisitor and ConstantExpressionValidator - Add validation for attribute arguments, class constants, property defaults - Support runtime factories for dynamic expressions in PHP 8.5 - Enable new expressions, object casts, closures in attribute contexts - Add proper scope handling for attribute factory functions - Integrate validation into preprocessing and stub generation pipeline - Add comprehensive tests for valid and invalid constant expressions - Update compiler base to handle attribute factory scope correctly - Remove old unsupported attribute argument validation logicpull/34/head
parent
2fe2344d88
commit
ea02ec096f
17 changed files with 1172 additions and 44 deletions
@ -0,0 +1,55 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Attribute(Attribute::TARGET_PROPERTY)] |
||||||
|
class PropertyAttributes |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
public readonly ?string $name = null, |
||||||
|
public readonly ?string $label = null, |
||||||
|
) {} |
||||||
|
} |
||||||
|
|
||||||
|
#[Attribute(Attribute::TARGET_PROPERTY)] |
||||||
|
class IntegerPropertyAttributes extends PropertyAttributes |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
?string $name = null, |
||||||
|
?string $label = null, |
||||||
|
public readonly ?int $default = null, |
||||||
|
public readonly ?int $min = null, |
||||||
|
public readonly ?int $max = null, |
||||||
|
public readonly ?int $step = null, |
||||||
|
public readonly ?object $object = null, |
||||||
|
) { |
||||||
|
parent::__construct($name, $label); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Attribute(Attribute::TARGET_PROPERTY)] |
||||||
|
class FloatPropertyAttributes extends PropertyAttributes |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
?string $name = null, |
||||||
|
?string $label = null, |
||||||
|
public readonly ?float $default = null, |
||||||
|
public readonly ?float $min = null, |
||||||
|
public readonly ?float $max = null, |
||||||
|
) { |
||||||
|
parent::__construct($name, $label); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class MyClass |
||||||
|
{ |
||||||
|
#[IntegerPropertyAttributes('prop', 'property: ', 5, 0, 10, 1, new stdClass())] |
||||||
|
public int $prop; |
||||||
|
} |
||||||
|
|
||||||
|
$refl = new ReflectionProperty('MyClass', 'prop'); |
||||||
|
$attributes = $refl->getAttributes(); |
||||||
|
|
||||||
|
foreach ($attributes as $attribute) { |
||||||
|
var_dump($attribute->getName()); |
||||||
|
var_dump($attribute->getArguments()); |
||||||
|
var_dump($attribute->newInstance()); |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
function loadClassConstantValue(): int |
||||||
|
{ |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
class ClassConstantInvalidExpression |
||||||
|
{ |
||||||
|
public const VALUE = loadClassConstantValue(); |
||||||
|
} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Attribute(Attribute::TARGET_CLASS)] |
||||||
|
final class PreprocessorInvalidExpressionAttribute |
||||||
|
{ |
||||||
|
public function __construct(public array $values) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function preprocessorLoadAttributeValue(): int |
||||||
|
{ |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
#[PreprocessorInvalidExpressionAttribute([1, [preprocessorLoadAttributeValue()]])] |
||||||
|
final class PreprocessorInvalidExpressionTarget |
||||||
|
{ |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
function loadPropertyDefaultValue(): int |
||||||
|
{ |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
class PropertyDefaultInvalidExpression |
||||||
|
{ |
||||||
|
public int $value = loadPropertyDefaultValue(); |
||||||
|
} |
||||||
@ -0,0 +1,278 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
use PhpParser\Node; |
||||||
|
use PhpParser\NodeTraverser; |
||||||
|
use PhpParser\ParserFactory; |
||||||
|
use PhpParser\PhpVersion; |
||||||
|
use TypePhp\Exception\SyntaxError; |
||||||
|
use TypePhp\Transform\ConstantExpressionValidator; |
||||||
|
use TypePhp\Transform\ConstantExpressionValidationVisitor; |
||||||
|
use TypePhp\Transform\RuntimeAttributeFactoryLowering; |
||||||
|
|
||||||
|
final class ConstantExpressionValidatorTest extends PHPUnit\Framework\TestCase |
||||||
|
{ |
||||||
|
/** @dataProvider validExpressionProvider */ |
||||||
|
public function testAcceptsPhpAttributeConstantExpressions(string $expression, string $phpVersion): void |
||||||
|
{ |
||||||
|
(new ConstantExpressionValidator($phpVersion))->validate( |
||||||
|
$this->parseAttributeExpression($expression, $phpVersion), |
||||||
|
true, |
||||||
|
); |
||||||
|
|
||||||
|
$this->addToAssertionCount(1); |
||||||
|
} |
||||||
|
|
||||||
|
public static function validExpressionProvider(): iterable |
||||||
|
{ |
||||||
|
yield 'literal array with unpack' => ['[1, ...VALUES, "key" => C::VALUE]', '8.4']; |
||||||
|
yield 'new expression' => ['new Value(flag: true)', '8.4']; |
||||||
|
yield 'new expression property fetch' => ['(new Value())->name', '8.4']; |
||||||
|
yield 'PHP 8.5 cast' => ['(object) ["value" => 1]', '8.5']; |
||||||
|
yield 'PHP 8.5 static closure' => ['static function (): int { return 1; }', '8.5']; |
||||||
|
yield 'PHP 8.5 function callable' => ['strlen(...)', '8.5']; |
||||||
|
yield 'PHP 8.5 static method callable' => ['Value::make(...)', '8.5']; |
||||||
|
yield 'short-circuited invalid right operand' => ['true || loadValue()', '8.5']; |
||||||
|
yield 'unselected invalid ternary branch' => ['true ? 1 : loadValue()', '8.5']; |
||||||
|
yield 'unselected invalid coalesce operand' => ['1 ?? loadValue()', '8.5']; |
||||||
|
} |
||||||
|
|
||||||
|
/** @dataProvider invalidExpressionProvider */ |
||||||
|
public function testRejectsExpressionsExactlyAsPhpDoes( |
||||||
|
string $expression, |
||||||
|
string $phpVersion, |
||||||
|
string $message, |
||||||
|
): void { |
||||||
|
$this->expectException(SyntaxError::class); |
||||||
|
$this->expectExceptionMessage($message); |
||||||
|
|
||||||
|
(new ConstantExpressionValidator($phpVersion))->validate( |
||||||
|
$this->parseAttributeExpression($expression, $phpVersion), |
||||||
|
true, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
public static function invalidExpressionProvider(): iterable |
||||||
|
{ |
||||||
|
yield 'nested function call' => [ |
||||||
|
'[1, [loadValue()]]', |
||||||
|
'8.5', |
||||||
|
'Constant expression contains invalid operations', |
||||||
|
]; |
||||||
|
yield 'ordinary call is invalid in PHP 8.5' => [ |
||||||
|
'strlen("value")', |
||||||
|
'8.5', |
||||||
|
'Constant expression contains invalid operations', |
||||||
|
]; |
||||||
|
yield 'PHP 8.5 pipe expression' => [ |
||||||
|
'"value" |> strlen(...)', |
||||||
|
'8.5', |
||||||
|
'Constant expression contains invalid operations', |
||||||
|
]; |
||||||
|
yield 'first-class callable before PHP 8.5' => [ |
||||||
|
'strlen(...)', |
||||||
|
'8.4', |
||||||
|
'Constant expression contains invalid operations', |
||||||
|
]; |
||||||
|
yield 'closure before PHP 8.5' => [ |
||||||
|
'static function (): int { return 1; }', |
||||||
|
'8.4', |
||||||
|
'Constant expression contains invalid operations', |
||||||
|
]; |
||||||
|
yield 'non-static closure' => [ |
||||||
|
'function (): int { return 1; }', |
||||||
|
'8.5', |
||||||
|
'Closures in constant expressions must be static', |
||||||
|
]; |
||||||
|
yield 'closure use' => [ |
||||||
|
'static function () use ($value): mixed { return $value; }', |
||||||
|
'8.5', |
||||||
|
'Cannot use(...) variables in constant expression', |
||||||
|
]; |
||||||
|
yield 'anonymous class' => [ |
||||||
|
'new class {}', |
||||||
|
'8.5', |
||||||
|
'Cannot use anonymous class in constant expression', |
||||||
|
]; |
||||||
|
yield 'dynamic class' => [ |
||||||
|
'new $className()', |
||||||
|
'8.5', |
||||||
|
'Cannot use dynamic class name in constant expression', |
||||||
|
]; |
||||||
|
yield 'new static' => [ |
||||||
|
'new static()', |
||||||
|
'8.5', |
||||||
|
'"static" is not allowed in compile-time constants', |
||||||
|
]; |
||||||
|
yield 'argument unpacking' => [ |
||||||
|
'new Value(...[])', |
||||||
|
'8.5', |
||||||
|
'Argument unpacking in constant expressions is not supported', |
||||||
|
]; |
||||||
|
yield 'empty array dimension' => [ |
||||||
|
'[1][]', |
||||||
|
'8.5', |
||||||
|
'Cannot use [] for reading', |
||||||
|
]; |
||||||
|
yield 'positional after named' => [ |
||||||
|
'new Value(first: 1, 2)', |
||||||
|
'8.5', |
||||||
|
'Cannot use positional argument after named argument', |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsUnpackingInTheAttributeArgumentList(): void |
||||||
|
{ |
||||||
|
$this->expectException(SyntaxError::class); |
||||||
|
$this->expectExceptionMessage('Cannot use unpacking in attribute argument list'); |
||||||
|
|
||||||
|
(new ConstantExpressionValidator('8.5'))->validateArguments( |
||||||
|
$this->parseAttributeArguments('...VALUES', '8.5'), |
||||||
|
true, |
||||||
|
true, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** @dataProvider runtimeFactoryExpressionProvider */ |
||||||
|
public function testPhp85DynamicExpressionsUseRuntimeFactories(string $expression): void |
||||||
|
{ |
||||||
|
$parser = (new ParserFactory())->createForVersion(PhpVersion::fromString('8.5')); |
||||||
|
$statements = $parser->parse("<?php #[Test({$expression})] class Target {}");
|
||||||
|
self::assertNotNull($statements); |
||||||
|
|
||||||
|
$traverser = new NodeTraverser(); |
||||||
|
$traverser->addVisitor(new ConstantExpressionValidationVisitor('8.5')); |
||||||
|
$traverser->addVisitor(new RuntimeAttributeFactoryLowering('test.php')); |
||||||
|
$statements = $traverser->traverse($statements); |
||||||
|
$class = $statements[0]; |
||||||
|
self::assertInstanceOf(Node\Stmt\Class_::class, $class); |
||||||
|
$value = $class->attrGroups[0]->attrs[0]->args[0]->value; |
||||||
|
|
||||||
|
self::assertNotSame('', $value->getAttribute(RuntimeAttributeFactoryLowering::FACTORY_NAME_ATTRIBUTE, '')); |
||||||
|
self::assertTrue($value->getAttribute(RuntimeAttributeFactoryLowering::FACTORY_LAZY_VALUE_ATTRIBUTE, false)); |
||||||
|
self::assertInstanceOf(Node\Stmt\Function_::class, $statements[1]); |
||||||
|
} |
||||||
|
|
||||||
|
public static function runtimeFactoryExpressionProvider(): iterable |
||||||
|
{ |
||||||
|
yield 'new' => ['new Value()']; |
||||||
|
yield 'object cast' => ['(object) ["value" => 1]']; |
||||||
|
yield 'static closure' => ['static function (): int { return 1; }']; |
||||||
|
yield 'function callable' => ['strlen(...)']; |
||||||
|
yield 'static method callable' => ['Value::make(...)']; |
||||||
|
} |
||||||
|
|
||||||
|
/** @dataProvider validDeclarationContextProvider */ |
||||||
|
public function testDeclarationContextsMatchPhpAllowDynamicRules(string $code, string $phpVersion): void |
||||||
|
{ |
||||||
|
$this->validateCode($code, $phpVersion); |
||||||
|
$this->addToAssertionCount(1); |
||||||
|
} |
||||||
|
|
||||||
|
public static function validDeclarationContextProvider(): iterable |
||||||
|
{ |
||||||
|
yield 'parameter default allows new' => ['function f($value = new Value()) {}', '8.4']; |
||||||
|
yield 'global const allows new' => ['const VALUE = new Value();', '8.4']; |
||||||
|
yield 'static variable allows new' => ['function f() { static $value = new Value(); }', '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']; |
||||||
|
} |
||||||
|
|
||||||
|
/** @dataProvider invalidDeclarationContextProvider */ |
||||||
|
public function testDeclarationContextsRejectDisallowedExpressions( |
||||||
|
string $code, |
||||||
|
string $phpVersion, |
||||||
|
string $message, |
||||||
|
): void { |
||||||
|
$this->expectException(SyntaxError::class); |
||||||
|
$this->expectExceptionMessage($message); |
||||||
|
$this->validateCode($code, $phpVersion); |
||||||
|
} |
||||||
|
|
||||||
|
public static function invalidDeclarationContextProvider(): iterable |
||||||
|
{ |
||||||
|
yield 'class constant rejects new' => [ |
||||||
|
'class C { const VALUE = new Value(); }', |
||||||
|
'8.4', |
||||||
|
'New expressions are not supported in this context', |
||||||
|
]; |
||||||
|
yield 'property rejects new' => [ |
||||||
|
'class C { public mixed $value = new Value(); }', |
||||||
|
'8.4', |
||||||
|
'New expressions are not supported in this context', |
||||||
|
]; |
||||||
|
yield 'enum case rejects new' => [ |
||||||
|
'enum E: int { case Value = new Value(); }', |
||||||
|
'8.4', |
||||||
|
'New expressions are not supported in this context', |
||||||
|
]; |
||||||
|
yield 'class constant rejects ordinary call' => [ |
||||||
|
'class C { const VALUE = loadValue(); }', |
||||||
|
'8.5', |
||||||
|
'Constant expression contains invalid operations', |
||||||
|
]; |
||||||
|
yield 'property rejects ordinary call' => [ |
||||||
|
'class C { public mixed $value = loadValue(); }', |
||||||
|
'8.5', |
||||||
|
'Constant expression contains invalid operations', |
||||||
|
]; |
||||||
|
yield 'property rejects PHP 8.5 object cast' => [ |
||||||
|
'class C { public mixed $value = (object) []; }', |
||||||
|
'8.5', |
||||||
|
'Object casts are not supported in this context', |
||||||
|
]; |
||||||
|
yield 'class constant rejects closure before PHP 8.5' => [ |
||||||
|
'class C { const VALUE = static function (): int { return 1; }; }', |
||||||
|
'8.4', |
||||||
|
'Constant expression contains invalid operations', |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public function testExposesPhpStyleNodeWhitelist(): void |
||||||
|
{ |
||||||
|
$php84 = new ConstantExpressionValidator('8.4'); |
||||||
|
$php85 = new ConstantExpressionValidator('8.5'); |
||||||
|
|
||||||
|
self::assertTrue($php84->isAllowedInConstantExpression( |
||||||
|
$this->parseAttributeExpression('new Value()', '8.4'), |
||||||
|
)); |
||||||
|
self::assertFalse($php84->isAllowedInConstantExpression( |
||||||
|
$this->parseAttributeExpression('strlen(...)', '8.4'), |
||||||
|
)); |
||||||
|
self::assertTrue($php85->isAllowedInConstantExpression( |
||||||
|
$this->parseAttributeExpression('strlen(...)', '8.5'), |
||||||
|
)); |
||||||
|
self::assertFalse($php85->isAllowedInConstantExpression( |
||||||
|
$this->parseAttributeExpression('"value" |> strlen(...)', '8.5'), |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
private function parseAttributeExpression(string $expression, string $phpVersion): Node\Expr |
||||||
|
{ |
||||||
|
return $this->parseAttributeArguments($expression, $phpVersion)[0]->value; |
||||||
|
} |
||||||
|
|
||||||
|
/** @return list<Node\Arg> */ |
||||||
|
private function parseAttributeArguments(string $arguments, string $phpVersion): array |
||||||
|
{ |
||||||
|
$parser = (new ParserFactory())->createForVersion(PhpVersion::fromString($phpVersion)); |
||||||
|
$statements = $parser->parse("<?php #[Test({$arguments})] class Target {}");
|
||||||
|
self::assertNotNull($statements); |
||||||
|
$class = $statements[0]; |
||||||
|
self::assertInstanceOf(Node\Stmt\Class_::class, $class); |
||||||
|
|
||||||
|
return $class->attrGroups[0]->attrs[0]->args; |
||||||
|
} |
||||||
|
|
||||||
|
private function validateCode(string $code, string $phpVersion): void |
||||||
|
{ |
||||||
|
$parser = (new ParserFactory())->createForVersion(PhpVersion::fromString($phpVersion)); |
||||||
|
$statements = $parser->parse("<?php {$code}");
|
||||||
|
self::assertNotNull($statements); |
||||||
|
$traverser = new NodeTraverser(); |
||||||
|
$traverser->addVisitor(new ConstantExpressionValidationVisitor($phpVersion)); |
||||||
|
$traverser->traverse($statements); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,83 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP. |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Transform; |
||||||
|
|
||||||
|
use PhpParser\Node; |
||||||
|
use PhpParser\NodeVisitorAbstract; |
||||||
|
|
||||||
|
/** |
||||||
|
* Applies the allow_dynamic values used by php-src at each declaration site. |
||||||
|
* |
||||||
|
* false: class constants, property defaults and enum cases. |
||||||
|
* true: attributes, parameter defaults, global constants and static variables. |
||||||
|
*/ |
||||||
|
final class ConstantExpressionValidationVisitor extends NodeVisitorAbstract |
||||||
|
{ |
||||||
|
private readonly ConstantExpressionValidator $validator; |
||||||
|
|
||||||
|
public function __construct(string $phpVersion) |
||||||
|
{ |
||||||
|
$this->validator = new ConstantExpressionValidator($phpVersion); |
||||||
|
} |
||||||
|
|
||||||
|
public function enterNode(Node $node): null |
||||||
|
{ |
||||||
|
if ($node instanceof Node\Attribute) { |
||||||
|
$this->validator->validateArguments( |
||||||
|
$node->args, |
||||||
|
allowDynamic: true, |
||||||
|
attributeArgumentList: true, |
||||||
|
); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if ($node instanceof Node\Stmt\ClassConst) { |
||||||
|
foreach ($node->consts as $constant) { |
||||||
|
$this->validator->validate($constant->value, allowDynamic: false); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if ($node instanceof Node\Stmt\Property) { |
||||||
|
foreach ($node->props as $property) { |
||||||
|
if ($property->default !== null) { |
||||||
|
$this->validator->validate($property->default, allowDynamic: false); |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if ($node instanceof Node\Stmt\EnumCase && $node->expr !== null) { |
||||||
|
$this->validator->validate($node->expr, allowDynamic: false); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if ($node instanceof Node\Param && $node->default !== null) { |
||||||
|
$this->validator->validate($node->default, allowDynamic: true); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if ($node instanceof Node\Stmt\Const_) { |
||||||
|
foreach ($node->consts as $constant) { |
||||||
|
$this->validator->validate($constant->value, allowDynamic: true); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if ($node instanceof Node\Stmt\Static_) { |
||||||
|
foreach ($node->vars as $variable) { |
||||||
|
if ($variable->default !== null) { |
||||||
|
$this->validator->validate($variable->default, allowDynamic: true); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,334 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP. |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Transform; |
||||||
|
|
||||||
|
use PhpParser\ConstExprEvaluator; |
||||||
|
use PhpParser\Node; |
||||||
|
use PhpParser\Node\Expr; |
||||||
|
use TypePhp\Exception\SyntaxError; |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements PHP's reusable constant-expression whitelist and validation. |
||||||
|
* |
||||||
|
* The allowDynamic context flag matches zend_const_expr_to_zval(): it controls |
||||||
|
* `new` and object casts, while the node whitelist remains shared by attributes, |
||||||
|
* constants, defaults and other constant-expression contexts. |
||||||
|
*/ |
||||||
|
final class ConstantExpressionValidator |
||||||
|
{ |
||||||
|
private readonly bool $php85; |
||||||
|
|
||||||
|
public function __construct(string $phpVersion) |
||||||
|
{ |
||||||
|
$this->php85 = version_compare($phpVersion, '8.5', '>='); |
||||||
|
} |
||||||
|
|
||||||
|
public function validate(Expr $expression, bool $allowDynamic = false): void |
||||||
|
{ |
||||||
|
$this->validateExpression($expression, $allowDynamic); |
||||||
|
} |
||||||
|
|
||||||
|
/** @param list<Node\Arg|Node\VariadicPlaceholder> $arguments */ |
||||||
|
public function validateArguments( |
||||||
|
array $arguments, |
||||||
|
bool $allowDynamic = false, |
||||||
|
bool $attributeArgumentList = false, |
||||||
|
): void |
||||||
|
{ |
||||||
|
$usesNamedArguments = false; |
||||||
|
$namedArguments = []; |
||||||
|
foreach ($arguments as $argument) { |
||||||
|
if (!$argument instanceof Node\Arg) { |
||||||
|
$this->invalidOperation(); |
||||||
|
} |
||||||
|
if ($argument->unpack) { |
||||||
|
throw new SyntaxError($attributeArgumentList |
||||||
|
? 'Cannot use unpacking in attribute argument list' |
||||||
|
: 'Argument unpacking in constant expressions is not supported'); |
||||||
|
} |
||||||
|
if ($argument->byRef) { |
||||||
|
$this->invalidOperation(); |
||||||
|
} |
||||||
|
if ($argument->name !== null) { |
||||||
|
$usesNamedArguments = true; |
||||||
|
if ($attributeArgumentList) { |
||||||
|
$name = $argument->name->toString(); |
||||||
|
if (isset($namedArguments[$name])) { |
||||||
|
throw new SyntaxError("Duplicate named parameter \${$name}"); |
||||||
|
} |
||||||
|
$namedArguments[$name] = true; |
||||||
|
} |
||||||
|
} elseif ($usesNamedArguments) { |
||||||
|
throw new SyntaxError('Cannot use positional argument after named argument'); |
||||||
|
} |
||||||
|
$this->validateExpression($argument->value, $allowDynamic); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** Equivalent to php-src's zend_is_allowed_in_const_expr(). */ |
||||||
|
public function isAllowedInConstantExpression(Expr $expression): bool |
||||||
|
{ |
||||||
|
return $expression instanceof Node\Scalar\Int_ |
||||||
|
|| $expression instanceof Node\Scalar\Float_ |
||||||
|
|| $expression instanceof Node\Scalar\String_ |
||||||
|
|| $expression instanceof Node\Scalar\MagicConst |
||||||
|
|| $expression instanceof Expr\ConstFetch |
||||||
|
|| ($expression instanceof Expr\BinaryOp |
||||||
|
&& !$expression instanceof Expr\BinaryOp\Pipe) |
||||||
|
|| $expression instanceof Expr\UnaryPlus |
||||||
|
|| $expression instanceof Expr\UnaryMinus |
||||||
|
|| $expression instanceof Expr\BooleanNot |
||||||
|
|| $expression instanceof Expr\BitwiseNot |
||||||
|
|| $expression instanceof Expr\Ternary |
||||||
|
|| $expression instanceof Expr\ArrayDimFetch |
||||||
|
|| $expression instanceof Expr\Array_ |
||||||
|
|| $expression instanceof Expr\ClassConstFetch |
||||||
|
|| $expression instanceof Expr\PropertyFetch |
||||||
|
|| $expression instanceof Expr\NullsafePropertyFetch |
||||||
|
|| $expression instanceof Expr\New_ |
||||||
|
|| ($this->php85 && ($expression instanceof Expr\Cast |
||||||
|
|| $expression instanceof Expr\Closure |
||||||
|
|| $expression instanceof Expr\FuncCall |
||||||
|
|| $expression instanceof Expr\StaticCall)); |
||||||
|
} |
||||||
|
|
||||||
|
private function validateExpression(Expr $expression, bool $allowDynamic): void |
||||||
|
{ |
||||||
|
if (!$this->isAllowedInConstantExpression($expression)) { |
||||||
|
$this->invalidOperation(); |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Node\Scalar |
||||||
|
|| $expression instanceof Expr\ConstFetch) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\BinaryOp\BooleanAnd |
||||||
|
|| $expression instanceof Expr\BinaryOp\LogicalAnd |
||||||
|
|| $expression instanceof Expr\BinaryOp\BooleanOr |
||||||
|
|| $expression instanceof Expr\BinaryOp\LogicalOr) { |
||||||
|
$this->validateExpression($expression->left, $allowDynamic); |
||||||
|
[$known, $left] = $this->tryEvaluate($expression->left); |
||||||
|
$isAnd = $expression instanceof Expr\BinaryOp\BooleanAnd |
||||||
|
|| $expression instanceof Expr\BinaryOp\LogicalAnd; |
||||||
|
if (!$known || ($isAnd ? (bool) $left : !(bool) $left)) { |
||||||
|
$this->validateExpression($expression->right, $allowDynamic); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\BinaryOp\Coalesce) { |
||||||
|
$this->validateExpression($expression->left, $allowDynamic); |
||||||
|
[$known, $left] = $this->tryEvaluate($expression->left); |
||||||
|
if (!$known || $left === null) { |
||||||
|
$this->validateExpression($expression->right, $allowDynamic); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\BinaryOp) { |
||||||
|
$this->validateExpression($expression->left, $allowDynamic); |
||||||
|
$this->validateExpression($expression->right, $allowDynamic); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\UnaryPlus |
||||||
|
|| $expression instanceof Expr\UnaryMinus |
||||||
|
|| $expression instanceof Expr\BooleanNot |
||||||
|
|| $expression instanceof Expr\BitwiseNot) { |
||||||
|
$this->validateExpression($expression->expr, $allowDynamic); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\Ternary) { |
||||||
|
$this->validateExpression($expression->cond, $allowDynamic); |
||||||
|
[$known, $condition] = $this->tryEvaluate($expression->cond); |
||||||
|
if ($known) { |
||||||
|
if ((bool) $condition) { |
||||||
|
if ($expression->if !== null) { |
||||||
|
$this->validateExpression($expression->if, $allowDynamic); |
||||||
|
} |
||||||
|
} else { |
||||||
|
$this->validateExpression($expression->else, $allowDynamic); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
if ($expression->if !== null) { |
||||||
|
$this->validateExpression($expression->if, $allowDynamic); |
||||||
|
} |
||||||
|
$this->validateExpression($expression->else, $allowDynamic); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\ArrayDimFetch) { |
||||||
|
$this->validateExpression($expression->var, $allowDynamic); |
||||||
|
if ($expression->dim === null) { |
||||||
|
throw new SyntaxError('Cannot use [] for reading'); |
||||||
|
} |
||||||
|
$this->validateExpression($expression->dim, $allowDynamic); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\Array_) { |
||||||
|
foreach ($expression->items as $item) { |
||||||
|
if ($item === null || $item->byRef) { |
||||||
|
$this->invalidOperation(); |
||||||
|
} |
||||||
|
if ($item->key !== null) { |
||||||
|
$this->validateExpression($item->key, $allowDynamic); |
||||||
|
} |
||||||
|
$this->validateExpression($item->value, $allowDynamic); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\ClassConstFetch) { |
||||||
|
$this->validateClassConstantFetch($expression, $allowDynamic); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\PropertyFetch |
||||||
|
|| $expression instanceof Expr\NullsafePropertyFetch) { |
||||||
|
$this->validateExpression($expression->var, $allowDynamic); |
||||||
|
if ($expression->name instanceof Expr) { |
||||||
|
$this->validateExpression($expression->name, $allowDynamic); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\New_) { |
||||||
|
if (!$allowDynamic) { |
||||||
|
throw new SyntaxError('New expressions are not supported in this context'); |
||||||
|
} |
||||||
|
$this->validateNew($expression, $allowDynamic); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->php85 && $expression instanceof Expr\Cast) { |
||||||
|
if ($expression instanceof Expr\Cast\Object_ && !$allowDynamic) { |
||||||
|
throw new SyntaxError('Object casts are not supported in this context'); |
||||||
|
} |
||||||
|
$this->validateExpression($expression->expr, $allowDynamic); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->php85 && $expression instanceof Expr\Closure) { |
||||||
|
if (!$expression->static) { |
||||||
|
throw new SyntaxError('Closures in constant expressions must be static'); |
||||||
|
} |
||||||
|
if ($expression->uses !== []) { |
||||||
|
throw new SyntaxError('Cannot use(...) variables in constant expression'); |
||||||
|
} |
||||||
|
// PHP compiles the closure body normally instead of treating its |
||||||
|
// statements as children of the surrounding constant expression. |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->php85 && ($expression instanceof Expr\FuncCall |
||||||
|
|| $expression instanceof Expr\StaticCall)) { |
||||||
|
$this->validateFirstClassCallable($expression); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$this->invalidOperation(); |
||||||
|
} |
||||||
|
|
||||||
|
private function validateClassConstantFetch( |
||||||
|
Expr\ClassConstFetch $expression, |
||||||
|
bool $allowDynamic, |
||||||
|
): void |
||||||
|
{ |
||||||
|
if (!$expression->class instanceof Node\Name) { |
||||||
|
if ($expression->name instanceof Node\Identifier |
||||||
|
&& strtolower($expression->name->toString()) === 'class') { |
||||||
|
throw new SyntaxError('(expression)::class cannot be used in constant expressions'); |
||||||
|
} |
||||||
|
throw new SyntaxError('Dynamic class names are not allowed in compile-time class constant references'); |
||||||
|
} |
||||||
|
|
||||||
|
$class = strtolower($expression->class->toString()); |
||||||
|
if ($class === 'static') { |
||||||
|
if ($expression->name instanceof Node\Identifier |
||||||
|
&& strtolower($expression->name->toString()) === 'class') { |
||||||
|
throw new SyntaxError('static::class cannot be used for compile-time class name resolution'); |
||||||
|
} |
||||||
|
throw new SyntaxError('"static::" is not allowed in compile-time constants'); |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression->name instanceof Expr) { |
||||||
|
$this->validateExpression($expression->name, $allowDynamic); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function validateNew(Expr\New_ $expression, bool $allowDynamic): void |
||||||
|
{ |
||||||
|
if ($expression->class instanceof Node\Stmt\Class_) { |
||||||
|
throw new SyntaxError('Cannot use anonymous class in constant expression'); |
||||||
|
} |
||||||
|
if (!$expression->class instanceof Node\Name) { |
||||||
|
throw new SyntaxError('Cannot use dynamic class name in constant expression'); |
||||||
|
} |
||||||
|
if (strtolower($expression->class->toString()) === 'static') { |
||||||
|
throw new SyntaxError('"static" is not allowed in compile-time constants'); |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression->isFirstClassCallable()) { |
||||||
|
throw new SyntaxError('Cannot create Closure for new expression'); |
||||||
|
} |
||||||
|
$this->validateArguments($expression->args, $allowDynamic); |
||||||
|
} |
||||||
|
|
||||||
|
/** @param Expr\FuncCall|Expr\StaticCall $expression */ |
||||||
|
private function validateFirstClassCallable(Expr $expression): void |
||||||
|
{ |
||||||
|
if (!$expression->isFirstClassCallable()) { |
||||||
|
$this->invalidOperation(); |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression instanceof Expr\FuncCall) { |
||||||
|
if (!$expression->name instanceof Node\Name) { |
||||||
|
throw new SyntaxError('Cannot use dynamic function name in constant expression'); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($expression->class instanceof Node\Stmt\Class_) { |
||||||
|
throw new SyntaxError('Cannot use anonymous class in constant expression'); |
||||||
|
} |
||||||
|
if (!$expression->class instanceof Node\Name) { |
||||||
|
throw new SyntaxError('Cannot use dynamic class name in constant expression'); |
||||||
|
} |
||||||
|
if (strtolower($expression->class->toString()) === 'static') { |
||||||
|
throw new SyntaxError('"static" is not allowed in compile-time constants'); |
||||||
|
} |
||||||
|
if (!$expression->name instanceof Node\Identifier) { |
||||||
|
throw new SyntaxError('Cannot use dynamic method name in constant expression'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function invalidOperation(): never |
||||||
|
{ |
||||||
|
throw new SyntaxError('Constant expression contains invalid operations'); |
||||||
|
} |
||||||
|
|
||||||
|
/** @return array{bool, mixed} */ |
||||||
|
private function tryEvaluate(Expr $expression): array |
||||||
|
{ |
||||||
|
try { |
||||||
|
$value = (new ConstExprEvaluator( |
||||||
|
static function (): never { |
||||||
|
throw new \LogicException('Expression is not statically known'); |
||||||
|
}, |
||||||
|
))->evaluateDirectly($expression); |
||||||
|
return [true, $value]; |
||||||
|
} catch (\Throwable) { |
||||||
|
return [false, null]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,212 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP. |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Transform; |
||||||
|
|
||||||
|
use PhpParser\Node; |
||||||
|
use PhpParser\Node\Expr; |
||||||
|
use PhpParser\Node\Stmt; |
||||||
|
use PhpParser\NodeTraverser; |
||||||
|
use PhpParser\NodeFinder; |
||||||
|
use PhpParser\NodeVisitor\CloningVisitor; |
||||||
|
use PhpParser\NodeVisitorAbstract; |
||||||
|
use PhpParser\PrettyPrinter\Standard; |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates hidden TypePHP functions for attribute values that Zend cannot |
||||||
|
* persistently construct during MINIT. Reflection invokes the compiled native |
||||||
|
* function at request time and receives an ordinary request-local php::Var. |
||||||
|
*/ |
||||||
|
final class RuntimeAttributeFactoryLowering extends NodeVisitorAbstract |
||||||
|
{ |
||||||
|
public const FACTORY_NAME_ATTRIBUTE = 'typephpRuntimeAttributeFactory'; |
||||||
|
public const FACTORY_FUNCTION_ATTRIBUTE = 'typephpRuntimeAttributeFactoryFunction'; |
||||||
|
public const FACTORY_SCOPE_ATTRIBUTE = 'typephpRuntimeAttributeFactoryScope'; |
||||||
|
public const FACTORY_LAZY_VALUE_ATTRIBUTE = 'typephpRuntimeAttributeFactoryLazyValue'; |
||||||
|
|
||||||
|
/** @var list<array{namespace: string, parent: string}> */ |
||||||
|
private array $classStack = []; |
||||||
|
/** @var list<Stmt\Function_> */ |
||||||
|
private array $globalFactories = []; |
||||||
|
/** @var list<list<Stmt\Function_>> */ |
||||||
|
private array $namespaceFactories = []; |
||||||
|
private string $namespace = ''; |
||||||
|
private int $sequence = 0; |
||||||
|
|
||||||
|
public function __construct(private readonly string $sourceFile = '') |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function enterNode(Node $node): null |
||||||
|
{ |
||||||
|
if ($node instanceof Stmt\Namespace_) { |
||||||
|
$this->namespace = $node->name?->toString() ?? ''; |
||||||
|
$this->namespaceFactories[] = []; |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if ($node instanceof Stmt\ClassLike) { |
||||||
|
$class = $node->getAttribute('namespacedName'); |
||||||
|
$parent = $node instanceof Stmt\Class_ ? $node->extends : null; |
||||||
|
$this->classStack[] = [ |
||||||
|
'namespace' => $class instanceof Node\Name |
||||||
|
? $class->toString() |
||||||
|
: ltrim($this->namespace . '\\' . ($node->name?->toString() ?? ''), '\\'), |
||||||
|
'parent' => $parent?->toString() ?? '', |
||||||
|
]; |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if (!$node instanceof Node\Attribute) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
if (CompileTimeAttributeRegistry::get($node->name->toString()) !== null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
foreach ($node->args as $argument) { |
||||||
|
if (!$this->requiresFactory($argument->value)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$factory = $this->createFactory($argument->value); |
||||||
|
$argument->value->setAttribute(self::FACTORY_NAME_ATTRIBUTE, $factory['fullName']); |
||||||
|
if ($this->requiresLazyValue($argument->value)) { |
||||||
|
$argument->value->setAttribute(self::FACTORY_LAZY_VALUE_ATTRIBUTE, true); |
||||||
|
} |
||||||
|
if ($this->namespaceFactories !== []) { |
||||||
|
$index = array_key_last($this->namespaceFactories); |
||||||
|
$this->namespaceFactories[$index][] = $factory['node']; |
||||||
|
} else { |
||||||
|
$this->globalFactories[] = $factory['node']; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public function leaveNode(Node $node): null |
||||||
|
{ |
||||||
|
if ($node instanceof Stmt\ClassLike) { |
||||||
|
array_pop($this->classStack); |
||||||
|
} elseif ($node instanceof Stmt\Namespace_) { |
||||||
|
$factories = array_pop($this->namespaceFactories); |
||||||
|
if ($factories !== []) { |
||||||
|
array_push($node->stmts, ...$factories); |
||||||
|
} |
||||||
|
$this->namespace = ''; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public function afterTraverse(array $nodes): ?array |
||||||
|
{ |
||||||
|
if ($this->globalFactories !== []) { |
||||||
|
array_push($nodes, ...$this->globalFactories); |
||||||
|
} |
||||||
|
return $nodes; |
||||||
|
} |
||||||
|
|
||||||
|
private function requiresFactory(Expr $value): bool |
||||||
|
{ |
||||||
|
return $this->requiresLazyValue($value) |
||||||
|
|| $value instanceof Expr\ConstFetch |
||||||
|
|| $value instanceof Expr\ClassConstFetch; |
||||||
|
} |
||||||
|
|
||||||
|
private function requiresLazyValue(Expr $value): bool |
||||||
|
{ |
||||||
|
if ($value instanceof Expr\Array_ && $value->items !== []) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return (new NodeFinder())->findFirst($value, static function (Node $node): bool { |
||||||
|
return $node instanceof Expr\New_ |
||||||
|
|| $node instanceof Expr\Closure |
||||||
|
|| $node instanceof Expr\Cast\Object_ |
||||||
|
|| (($node instanceof Expr\FuncCall || $node instanceof Expr\StaticCall) |
||||||
|
&& $node->isFirstClassCallable()); |
||||||
|
}) !== null; |
||||||
|
} |
||||||
|
|
||||||
|
/** @return array{fullName: string, node: Stmt\Function_} */ |
||||||
|
private function createFactory(Expr $value): array |
||||||
|
{ |
||||||
|
$position = $value->getStartFilePos() . ':' . $value->getEndFilePos(); |
||||||
|
$hash = substr(sha1($this->sourceFile . ':' . $position . ':' . $this->sequence++), 0, 20); |
||||||
|
$name = '__typephp_attribute_factory_' . $hash; |
||||||
|
$fullName = $this->namespace === '' ? $name : $this->namespace . '\\' . $name; |
||||||
|
$expression = $this->cloneExpression($value); |
||||||
|
$description = (new Standard())->prettyPrintExpr($expression); |
||||||
|
$describeVariable = new Expr\Variable('__typephpDescribe'); |
||||||
|
|
||||||
|
$function = new Stmt\Function_( |
||||||
|
new Node\Identifier($name), |
||||||
|
[ |
||||||
|
'params' => [new Node\Param( |
||||||
|
var: $describeVariable, |
||||||
|
type: new Node\Identifier('bool'), |
||||||
|
)], |
||||||
|
'returnType' => new Node\Identifier('mixed'), |
||||||
|
'stmts' => [ |
||||||
|
new Stmt\If_($describeVariable, [ |
||||||
|
'stmts' => [new Stmt\Return_(new Node\Scalar\String_($description))], |
||||||
|
]), |
||||||
|
new Stmt\Return_($expression), |
||||||
|
], |
||||||
|
], |
||||||
|
$value->getAttributes(), |
||||||
|
); |
||||||
|
$function->setAttribute(self::FACTORY_FUNCTION_ATTRIBUTE, true); |
||||||
|
if ($this->classStack !== []) { |
||||||
|
$context = $this->classStack[array_key_last($this->classStack)]; |
||||||
|
$function->setAttribute(self::FACTORY_SCOPE_ATTRIBUTE, $context['namespace']); |
||||||
|
} |
||||||
|
$function->namespacedName = new Node\Name($fullName); |
||||||
|
|
||||||
|
return ['fullName' => $fullName, 'node' => $function]; |
||||||
|
} |
||||||
|
|
||||||
|
private function cloneExpression(Expr $expression): Expr |
||||||
|
{ |
||||||
|
$traverser = new NodeTraverser(); |
||||||
|
$traverser->addVisitor(new CloningVisitor()); |
||||||
|
$context = $this->classStack === [] |
||||||
|
? ['namespace' => '', 'parent' => ''] |
||||||
|
: $this->classStack[array_key_last($this->classStack)]; |
||||||
|
$traverser->addVisitor(new class($context['namespace'], $context['parent']) extends NodeVisitorAbstract { |
||||||
|
public function __construct( |
||||||
|
private readonly string $class, |
||||||
|
private readonly string $parent, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
public function enterNode(Node $node): ?Node |
||||||
|
{ |
||||||
|
if (($node instanceof Expr\ClassConstFetch || $node instanceof Expr\New_) |
||||||
|
&& $node->class instanceof Node\Name) { |
||||||
|
$name = strtolower($node->class->toString()); |
||||||
|
if ($name === 'self' || $name === 'static') { |
||||||
|
$node->class = new Node\Name\FullyQualified($this->class); |
||||||
|
} elseif ($name === 'parent' && $this->parent !== '') { |
||||||
|
$node->class = new Node\Name\FullyQualified($this->parent); |
||||||
|
} |
||||||
|
} |
||||||
|
if ($node instanceof Node\Name) { |
||||||
|
$resolved = $node->getAttribute('resolvedName'); |
||||||
|
if ($resolved instanceof Node\Name) { |
||||||
|
return new Node\Name\FullyQualified($resolved->toString(), $resolved->getAttributes()); |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
}); |
||||||
|
/** @var Expr $clone */ |
||||||
|
[$clone] = $traverser->traverse([$expression]); |
||||||
|
return $clone; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue