- Add printerGenerated property to ClassDef entity - Implement removeMethod function in ClassDef for method removal - Add comprehensive tests for getter, setter, with, printer and notnull attributes - Create new CompileTimeAttribute utility class for attribute handling - Implement GetterLowering for generating getter methods from attributes - Add NotNullLowering for validating non-null parameters - Implement PrinterLowering for generating toString methods - Add PropertyMethodLowering for setter and with method generation - Update gen_stub.php to recognize new compile-time attributes - Enhance Preprocessor with printer generation logic - Add symbol repository function removal capability - Update Translator with printer generation predicate - Modify Visitor to handle new attribute transformations - Add polyfill definitions for Getter, Setter, Withpull/34/head
parent
50cf8c6655
commit
8bcd6ce863
20 changed files with 812 additions and 3 deletions
@ -0,0 +1,95 @@ |
||||
<?php |
||||
|
||||
namespace CompileTimeAttributes; |
||||
|
||||
use \Getter; |
||||
use \NotNull; |
||||
use \Printer; |
||||
use \Setter; |
||||
use \With; |
||||
|
||||
class PrintableBase |
||||
{ |
||||
public int $baseId = 1; |
||||
|
||||
protected string $ignored = 'hidden'; |
||||
} |
||||
|
||||
#[Printer] |
||||
class User extends PrintableBase |
||||
{ |
||||
public int $id = 2; |
||||
|
||||
public string $name = '张三'; |
||||
|
||||
#[Getter, Setter, With] |
||||
private string $nickname = 'typephp'; |
||||
|
||||
public function rename(#[NotNull] string $name): void |
||||
{ |
||||
$this->name = $name; |
||||
} |
||||
} |
||||
|
||||
class CustomPrinterBase |
||||
{ |
||||
public function toString(): string |
||||
{ |
||||
return 'custom'; |
||||
} |
||||
} |
||||
|
||||
#[Printer] |
||||
class CustomPrinterChild extends CustomPrinterBase |
||||
{ |
||||
public int $value = 1; |
||||
} |
||||
|
||||
#[Printer] |
||||
class LatePrinterChild extends LatePrinterBase |
||||
{ |
||||
public int $value = 1; |
||||
} |
||||
|
||||
class LatePrinterBase |
||||
{ |
||||
public function toString(): string |
||||
{ |
||||
return 'late'; |
||||
} |
||||
} |
||||
|
||||
class PromotedProperties |
||||
{ |
||||
public function __construct( |
||||
#[Getter, Setter, With] |
||||
private int $value, |
||||
) { |
||||
} |
||||
} |
||||
|
||||
function requireValue(#[NotNull] int $value): int |
||||
{ |
||||
return $value; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$requireName = function (#[NotNull] string $name): string { |
||||
return $name; |
||||
}; |
||||
$user = new User(); |
||||
$user->setNickname('php'); |
||||
$copy = $user->withNickname('cpp'); |
||||
echo $user->getNickname(); |
||||
echo $copy->getNickname(); |
||||
echo $user->toString(); |
||||
echo (new CustomPrinterChild())->toString(); |
||||
echo (new LatePrinterChild())->toString(); |
||||
echo requireValue(1); |
||||
echo $requireName('typephp'); |
||||
|
||||
$promoted = new PromotedProperties(1); |
||||
$promoted->setValue(2); |
||||
echo $promoted->withValue(3)->getValue(); |
||||
} |
||||
@ -0,0 +1,7 @@ |
||||
<?php |
||||
|
||||
#[Getter] |
||||
function invalidGetterTarget(): int |
||||
{ |
||||
return 1; |
||||
} |
||||
@ -0,0 +1,7 @@ |
||||
<?php |
||||
|
||||
class InvalidStaticGetter |
||||
{ |
||||
#[Getter] |
||||
public static int $value = 1; |
||||
} |
||||
@ -0,0 +1,37 @@ |
||||
<?php |
||||
|
||||
namespace App; |
||||
|
||||
use \Getter; |
||||
|
||||
class User |
||||
{ |
||||
#[Getter] |
||||
private string $name = 'Alice'; |
||||
|
||||
#[\Getter] |
||||
protected int $age = 18; |
||||
|
||||
#[Getter] |
||||
public string $title = 'developer'; |
||||
|
||||
#[Getter] |
||||
public int $x = 1, $y = 2; |
||||
|
||||
public function __construct( |
||||
#[Getter] |
||||
private bool $active = true, |
||||
) { |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$user = new User(); |
||||
var_dump($user->getName()); |
||||
var_dump($user->getAge()); |
||||
var_dump($user->getTitle()); |
||||
var_dump($user->getX()); |
||||
var_dump($user->getY()); |
||||
var_dump($user->getActive()); |
||||
} |
||||
@ -0,0 +1,6 @@ |
||||
<?php |
||||
|
||||
#[NotNull] |
||||
function invalidNotNullTarget(): void |
||||
{ |
||||
} |
||||
@ -0,0 +1,68 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\Transform; |
||||
|
||||
use PhpParser\Node; |
||||
use TypePhp\Exception\SyntaxError; |
||||
|
||||
final class CompileTimeAttribute |
||||
{ |
||||
public static function has(Node $node, string $name): bool |
||||
{ |
||||
if (!property_exists($node, 'attrGroups')) { |
||||
return false; |
||||
} |
||||
foreach ($node->attrGroups as $group) { |
||||
foreach ($group->attrs as $attribute) { |
||||
if (self::is($attribute, $name)) { |
||||
self::validateArguments($attribute, $name); |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
public static function consume(Node $node, string $name): bool |
||||
{ |
||||
$found = false; |
||||
foreach ($node->attrGroups as $groupIndex => $group) { |
||||
foreach ($group->attrs as $attributeIndex => $attribute) { |
||||
if (!self::is($attribute, $name)) { |
||||
continue; |
||||
} |
||||
self::validateArguments($attribute, $name); |
||||
$found = true; |
||||
unset($group->attrs[$attributeIndex]); |
||||
} |
||||
$group->attrs = array_values($group->attrs); |
||||
if ($group->attrs === []) { |
||||
unset($node->attrGroups[$groupIndex]); |
||||
} |
||||
} |
||||
$node->attrGroups = array_values($node->attrGroups); |
||||
return $found; |
||||
} |
||||
|
||||
public static function is(Node\Attribute $attribute, string $name): bool |
||||
{ |
||||
$resolvedName = $attribute->name->getAttribute('resolvedName') |
||||
?? $attribute->name->getAttribute('namespacedName') |
||||
?? $attribute->name; |
||||
|
||||
return strcasecmp(ltrim($resolvedName->toString(), '\\'), $name) === 0; |
||||
} |
||||
|
||||
private static function validateArguments(Node\Attribute $attribute, string $name): void |
||||
{ |
||||
if ($attribute->args !== []) { |
||||
throw new SyntaxError($name . ' does not accept arguments'); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,79 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\Transform; |
||||
|
||||
use PhpParser\Modifiers; |
||||
use PhpParser\Node; |
||||
use PhpParser\Node\Expr; |
||||
use PhpParser\Node\Param; |
||||
use PhpParser\Node\Stmt; |
||||
use TypePhp\Exception\SyntaxError; |
||||
|
||||
final class GetterLowering |
||||
{ |
||||
public static function validateTarget(Node $node): void |
||||
{ |
||||
if (!CompileTimeAttribute::has($node, 'Getter')) { |
||||
return; |
||||
} |
||||
|
||||
if ($node instanceof Stmt\Property) { |
||||
if ($node->isStatic()) { |
||||
throw new SyntaxError('Getter can only be applied to instance properties'); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
if ($node instanceof Param && $node->isPromoted()) { |
||||
return; |
||||
} |
||||
|
||||
throw new SyntaxError('Getter can only be applied to instance properties'); |
||||
} |
||||
|
||||
/** @return list<Stmt\ClassMethod> */ |
||||
public static function lowerProperty(Stmt\Property $property): array |
||||
{ |
||||
if (!CompileTimeAttribute::consume($property, 'Getter')) { |
||||
return []; |
||||
} |
||||
|
||||
$methods = []; |
||||
foreach ($property->props as $prop) { |
||||
$methods[] = self::createGetter( |
||||
$prop->name->toString(), |
||||
$property->type, |
||||
$property->getAttributes(), |
||||
); |
||||
} |
||||
return $methods; |
||||
} |
||||
|
||||
public static function lowerPromotedProperty(Param $param): ?Stmt\ClassMethod |
||||
{ |
||||
if (!$param->isPromoted() || !is_string($param->var->name) || !CompileTimeAttribute::consume($param, 'Getter')) { |
||||
return null; |
||||
} |
||||
|
||||
return self::createGetter($param->var->name, $param->type, $param->getAttributes()); |
||||
} |
||||
|
||||
private static function createGetter(string $property, ?Node $type, array $attributes): Stmt\ClassMethod |
||||
{ |
||||
return new Stmt\ClassMethod('get' . ucfirst($property), [ |
||||
'flags' => Modifiers::PUBLIC, |
||||
'returnType' => $type === null ? null : clone $type, |
||||
'stmts' => [new Stmt\Return_(new Expr\PropertyFetch( |
||||
new Expr\Variable('this'), |
||||
$property, |
||||
))], |
||||
], $attributes); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,57 @@ |
||||
<?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\Param; |
||||
use PhpParser\Node\Stmt; |
||||
use TypePhp\Exception\SyntaxError; |
||||
|
||||
final class NotNullLowering |
||||
{ |
||||
public static function validateTarget(Node $node): void |
||||
{ |
||||
if (CompileTimeAttribute::has($node, 'NotNull') && !$node instanceof Param) { |
||||
throw new SyntaxError('NotNull can only be applied to function or method parameters'); |
||||
} |
||||
} |
||||
|
||||
public static function lowerFunction(Stmt\Function_|Stmt\ClassMethod|Expr\Closure $function): void |
||||
{ |
||||
$checks = []; |
||||
foreach ($function->params as $param) { |
||||
if (!CompileTimeAttribute::consume($param, 'NotNull')) { |
||||
continue; |
||||
} |
||||
if ($function->stmts === null || !is_string($param->var->name)) { |
||||
throw new SyntaxError('NotNull requires a concrete function or method parameter'); |
||||
} |
||||
$name = $param->var->name; |
||||
$checks[] = new Stmt\If_(new Expr\Empty_(new Expr\Variable($name)), [ |
||||
'stmts' => [new Stmt\Expression(new Expr\Throw_(new Expr\New_( |
||||
new Node\Name\FullyQualified('ValueError'), |
||||
[new Node\Arg(new Node\Scalar\String_('Parameter $' . $name . ' must not be empty'))], |
||||
)))], |
||||
]); |
||||
} |
||||
if ($checks !== []) { |
||||
$function->stmts = [...$checks, ...$function->stmts]; |
||||
} |
||||
} |
||||
|
||||
public static function rejectArrowFunction(Expr\ArrowFunction $function): void |
||||
{ |
||||
foreach ($function->params as $param) { |
||||
if (CompileTimeAttribute::has($param, 'NotNull')) { |
||||
throw new SyntaxError('NotNull is not supported on arrow function parameters'); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,103 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\Transform; |
||||
|
||||
use PhpParser\Modifiers; |
||||
use PhpParser\Node; |
||||
use PhpParser\Node\Expr; |
||||
use PhpParser\Node\Stmt; |
||||
use TypePhp\Exception\SyntaxError; |
||||
|
||||
final class PrinterLowering |
||||
{ |
||||
public const GENERATED_ATTRIBUTE = 'typephpPrinterGenerated'; |
||||
|
||||
public static function validateTarget(Node $node): void |
||||
{ |
||||
if (!CompileTimeAttribute::has($node, 'Printer')) { |
||||
return; |
||||
} |
||||
if (!$node instanceof Stmt\Class_ || $node->name === null) { |
||||
throw new SyntaxError('Printer can only be applied to named classes'); |
||||
} |
||||
} |
||||
|
||||
public static function lowerClass(Stmt\Class_ $class, bool $generate = true): void |
||||
{ |
||||
if (!CompileTimeAttribute::consume($class, 'Printer') || !$generate) { |
||||
return; |
||||
} |
||||
foreach ($class->getMethods() as $method) { |
||||
if ($method->name->toLowerString() === 'tostring') { |
||||
return; |
||||
} |
||||
} |
||||
|
||||
self::appendGeneratedMethod($class, self::ownPublicProperties($class)); |
||||
} |
||||
|
||||
/** @param list<string> $properties */ |
||||
public static function rebuildGeneratedMethod(Stmt\Class_ $class, array $properties): void |
||||
{ |
||||
self::removeGeneratedMethod($class); |
||||
self::appendGeneratedMethod($class, array_values(array_unique($properties))); |
||||
} |
||||
|
||||
public static function removeGeneratedMethod(Stmt\Class_ $class): void |
||||
{ |
||||
foreach ($class->stmts as $index => $stmt) { |
||||
if ($stmt instanceof Stmt\ClassMethod && $stmt->getAttribute(self::GENERATED_ATTRIBUTE)) { |
||||
unset($class->stmts[$index]); |
||||
} |
||||
} |
||||
$class->stmts = array_values($class->stmts); |
||||
} |
||||
|
||||
/** @return list<string> */ |
||||
public static function ownPublicProperties(Stmt\Class_ $class): array |
||||
{ |
||||
$properties = []; |
||||
foreach ($class->stmts as $stmt) { |
||||
if ($stmt instanceof Stmt\Property && $stmt->isPublic() && !$stmt->isStatic()) { |
||||
foreach ($stmt->props as $property) { |
||||
$properties[] = $property->name->toString(); |
||||
} |
||||
} |
||||
if ($stmt instanceof Stmt\ClassMethod && $stmt->name->toLowerString() === '__construct') { |
||||
foreach ($stmt->params as $param) { |
||||
if ($param->isPromoted() && ($param->flags & Modifiers::PUBLIC) && is_string($param->var->name)) { |
||||
$properties[] = $param->var->name; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return $properties; |
||||
} |
||||
|
||||
/** @param list<string> $properties */ |
||||
private static function appendGeneratedMethod(Stmt\Class_ $class, array $properties): void |
||||
{ |
||||
$expression = new Node\Scalar\String_($class->name->toString() . '('); |
||||
foreach ($properties as $index => $property) { |
||||
$prefix = ($index === 0 ? '' : ', ') . $property . '='; |
||||
$expression = new Expr\BinaryOp\Concat( |
||||
new Expr\BinaryOp\Concat($expression, new Node\Scalar\String_($prefix)), |
||||
new Expr\PropertyFetch(new Expr\Variable('this'), $property), |
||||
); |
||||
} |
||||
$expression = new Expr\BinaryOp\Concat($expression, new Node\Scalar\String_(')')); |
||||
$method = new Stmt\ClassMethod('toString', [ |
||||
'flags' => Modifiers::PUBLIC, |
||||
'returnType' => new Node\Identifier('string'), |
||||
'stmts' => [new Stmt\Return_($expression)], |
||||
]); |
||||
$method->setAttribute(self::GENERATED_ATTRIBUTE, true); |
||||
$class->stmts[] = $method; |
||||
} |
||||
} |
||||
@ -0,0 +1,114 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\Transform; |
||||
|
||||
use PhpParser\Modifiers; |
||||
use PhpParser\Node; |
||||
use PhpParser\Node\Expr; |
||||
use PhpParser\Node\Param; |
||||
use PhpParser\Node\Stmt; |
||||
use TypePhp\Exception\SyntaxError; |
||||
|
||||
final class PropertyMethodLowering |
||||
{ |
||||
private const ATTRIBUTES = ['Setter', 'With']; |
||||
|
||||
public static function validateTarget(Node $node): void |
||||
{ |
||||
foreach (self::ATTRIBUTES as $attribute) { |
||||
if (!CompileTimeAttribute::has($node, $attribute)) { |
||||
continue; |
||||
} |
||||
if ($node instanceof Stmt\Property && !$node->isStatic()) { |
||||
continue; |
||||
} |
||||
if ($node instanceof Param && $node->isPromoted()) { |
||||
continue; |
||||
} |
||||
throw new SyntaxError($attribute . ' can only be applied to instance properties'); |
||||
} |
||||
} |
||||
|
||||
/** @return list<Stmt\ClassMethod> */ |
||||
public static function lowerProperty(Stmt\Property $property): array |
||||
{ |
||||
$setter = CompileTimeAttribute::consume($property, 'Setter'); |
||||
$with = CompileTimeAttribute::consume($property, 'With'); |
||||
if (!$setter && !$with) { |
||||
return []; |
||||
} |
||||
|
||||
$methods = []; |
||||
foreach ($property->props as $prop) { |
||||
$name = $prop->name->toString(); |
||||
if ($setter) { |
||||
$methods[] = self::createSetter($name, $property->type, $property->getAttributes()); |
||||
} |
||||
if ($with) { |
||||
$methods[] = self::createWith($name, $property->type, $property->getAttributes()); |
||||
} |
||||
} |
||||
return $methods; |
||||
} |
||||
|
||||
/** @return list<Stmt\ClassMethod> */ |
||||
public static function lowerPromotedProperty(Param $param): array |
||||
{ |
||||
if (!$param->isPromoted() || !is_string($param->var->name)) { |
||||
return []; |
||||
} |
||||
$setter = CompileTimeAttribute::consume($param, 'Setter'); |
||||
$with = CompileTimeAttribute::consume($param, 'With'); |
||||
if (!$setter && !$with) { |
||||
return []; |
||||
} |
||||
|
||||
$methods = []; |
||||
if ($setter) { |
||||
$methods[] = self::createSetter($param->var->name, $param->type, $param->getAttributes()); |
||||
} |
||||
if ($with) { |
||||
$methods[] = self::createWith($param->var->name, $param->type, $param->getAttributes()); |
||||
} |
||||
return $methods; |
||||
} |
||||
|
||||
private static function createSetter(string $property, ?Node $type, array $attributes): Stmt\ClassMethod |
||||
{ |
||||
return new Stmt\ClassMethod('set' . ucfirst($property), [ |
||||
'flags' => Modifiers::PUBLIC, |
||||
'params' => [new Param(new Expr\Variable($property), type: $type === null ? null : clone $type)], |
||||
'returnType' => new Node\Identifier('void'), |
||||
'stmts' => [new Stmt\Expression(new Expr\Assign( |
||||
new Expr\PropertyFetch(new Expr\Variable('this'), $property), |
||||
new Expr\Variable($property), |
||||
))], |
||||
], $attributes); |
||||
} |
||||
|
||||
private static function createWith(string $property, ?Node $type, array $attributes): Stmt\ClassMethod |
||||
{ |
||||
return new Stmt\ClassMethod('with' . ucfirst($property), [ |
||||
'flags' => Modifiers::PUBLIC, |
||||
'params' => [new Param(new Expr\Variable($property), type: $type === null ? null : clone $type)], |
||||
'returnType' => new Node\Name('static'), |
||||
'stmts' => [ |
||||
new Stmt\Expression(new Expr\Assign( |
||||
new Expr\Variable('clone'), |
||||
new Expr\Clone_(new Expr\Variable('this')), |
||||
)), |
||||
new Stmt\Expression(new Expr\Assign( |
||||
new Expr\PropertyFetch(new Expr\Variable('clone'), $property), |
||||
new Expr\Variable($property), |
||||
)), |
||||
new Stmt\Return_(new Expr\Variable('clone')), |
||||
], |
||||
], $attributes); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue