feat(entity): add compile-time attributes and printer functionality

- 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, With
pull/34/head
韩天峰 1 month ago
parent 50cf8c6655
commit 8bcd6ce863
  1. 95
      phpunit/code/compile_time_attributes.php
  2. 12
      phpunit/code/compiler_api/library_import_php.php
  3. 7
      phpunit/code/getter-function.php
  4. 7
      phpunit/code/getter-static-property.php
  5. 37
      phpunit/code/getter.php
  6. 6
      phpunit/code/not-null-invalid-target.php
  7. 31
      phpunit/src/ClassTest.php
  8. 28
      phpunit/src/CompilerBaseApiTest.php
  9. 10
      src/Entity/ClassDef.php
  10. 78
      src/Preprocessor.php
  11. 1
      src/Symbol/SymbolRepository.php
  12. 68
      src/Transform/CompileTimeAttribute.php
  13. 79
      src/Transform/GetterLowering.php
  14. 57
      src/Transform/NotNullLowering.php
  15. 103
      src/Transform/PrinterLowering.php
  16. 114
      src/Transform/PropertyMethodLowering.php
  17. 33
      src/Transform/Visitor.php
  18. 12
      src/Translator.php
  19. 12
      src/gen_stub.php
  20. 25
      src/polyfills.php

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

@ -3,12 +3,19 @@
namespace LibraryApi;
use \ExtensionProvider as Provider;
use \Getter;
use \NotNull;
use \NoExport as Internal;
use \Printer;
use \Setter;
use \Type;
use \With;
#[Printer]
class Counter
{
public const int STEP = 2;
#[Getter, Setter, With]
public int $value = 1;
public int $doubled {
get {
@ -25,6 +32,11 @@ class Counter
return $this->value;
}
public function label(#[NotNull] string $value): string
{
return $value;
}
#[Internal]
public function reset(): void
{

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

@ -2,6 +2,37 @@
class ClassTest extends \BaseTest
{
public function testGetterGeneratesPublicMethodsForInstanceProperties(): void
{
$this->compile('getter.php');
}
public function testGetterRejectsStaticProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Getter can only be applied to instance properties');
$this->compile('getter-static-property.php');
}
public function testGetterRejectsNonPropertyTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Getter can only be applied to instance properties');
$this->compile('getter-function.php');
}
public function testCompileTimeGeneratedPropertyMethodsPrinterAndNotNull(): void
{
$this->compile('compile_time_attributes.php');
}
public function testNotNullRejectsNonParameterTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('NotNull can only be applied to function or method parameters');
$this->compile('not-null-invalid-target.php');
}
public function testReAssignThis()
{
$this->exec('Cannot re-assign $this', 're-assign-this.php');

@ -933,6 +933,16 @@ YAML);
'NoExport',
file_get_contents($this->compiler->getArgInfoHeaderFile($file)),
);
$this->assertStringNotContainsString(
'Getter',
file_get_contents($this->compiler->getArgInfoHeaderFile($file)),
);
foreach (['NotNull', 'Printer', 'Setter', 'With'] as $attribute) {
$this->assertStringNotContainsString(
$attribute,
file_get_contents($this->compiler->getArgInfoHeaderFile($file)),
);
}
}
$provider = $this->invokeMethod('getClass', 'LibraryApi\\InternalStringExtension');
$this->assertSame(Type::STR, $provider->extensionProviderTarget);
@ -945,11 +955,18 @@ YAML);
$this->assertStringContainsString('class Counter', $stub);
$this->assertStringContainsString('public const int STEP = 2;', $stub);
$this->assertStringContainsString('public int $value = 1;', $stub);
$this->assertStringContainsString('#[\Getter, \Setter, \With]', $stub);
$this->assertStringContainsString('#[\Printer]', $stub);
$this->assertStringContainsString('#[\NotNull]', $stub);
$this->assertMatchesRegularExpression(
'/public int \$doubled\s*\{\s*get\s*\{\s*\}\s*set\(int \$value\)\s*\{\s*\}\s*\}/s',
$stub,
);
$this->assertStringContainsString('function add(int $amount = self::STEP): int', $stub);
$this->assertMatchesRegularExpression(
'/function label\(\s*#\[\\\\NotNull\]\s*string \$value\s*\): string/s',
$stub,
);
$this->assertStringContainsString('function twice(int $value): int', $stub);
$this->assertStringContainsString('function native_value(string $name = \'typephp\'): string', $stub);
$this->assertStringContainsString('class NativeCounter', $stub);
@ -972,6 +989,10 @@ YAML);
'TYPEPHP_PRIME2_API php::Int php_libraryapi__twice(',
$libraryHeader,
);
$this->assertStringContainsString(
'TYPEPHP_PRIME2_API php::Int php_libraryapi__counter__getvalue(',
$libraryHeader,
);
$this->assertStringContainsString(
'extern php::Int php_libraryapi__internal_twice(',
$libraryHeader,
@ -1021,6 +1042,10 @@ YAML);
'TYPEPHP_PRIME2_IMPORT php::Int php_libraryapi__counter__add(',
$header,
);
$this->assertStringContainsString(
'TYPEPHP_PRIME2_IMPORT php::Int php_libraryapi__counter__getvalue(',
$header,
);
$this->assertStringContainsString(
'TYPEPHP_PRIME2_IMPORT php::Int php_libraryapi__twice(',
$header,
@ -1044,6 +1069,8 @@ YAML);
$stubCppCode = file_get_contents($stubCpp);
$this->assertStringContainsString('ZEND_METHOD(LibraryApi_Counter, add)', $stubCppCode);
$this->assertStringContainsString('ZEND_METHOD(LibraryApi_Counter, getValue)', $stubCppCode);
$this->assertStringContainsString('php_libraryapi__counter__getvalue(this_)', $stubCppCode);
$this->assertStringContainsString('php_libraryapi__counter__add(this_, arg_amount)', $stubCppCode);
$this->assertStringNotContainsString(
'php::Int php_libraryapi__counter__add(php::Object &this_',
@ -1052,6 +1079,7 @@ YAML);
$arginfoFile = $consumer->getArgInfoHeaderFile($stubFile);
$arginfo = file_get_contents($arginfoFile);
$this->assertStringNotContainsString('Getter', $arginfo);
$this->assertStringContainsString('const_STEP_value', $arginfo);
$this->assertStringContainsString('property_value_default_value', $arginfo);
$this->assertSame(['prime2'], $consumer->getLinkLibs());

@ -35,6 +35,8 @@ class ClassDef extends ClassLikeDef
/** Whether this class and its methods are part of the public ABI of a library build. */
public bool $exported = true;
public ?string $extensionProviderTarget = null;
/** Whether #[Printer] generated this class's own toString() method. */
public bool $printerGenerated = false;
/**
* Backing type for backed enums ('int' or 'string'), null for pure enums.
@ -102,6 +104,14 @@ class ClassDef extends ClassLikeDef
return isset($this->methods[strtolower($method)]);
}
public function removeMethod(string $method): ?MethodDef
{
$name = strtolower($method);
$methodDef = $this->methods[$name] ?? null;
unset($this->methods[$name]);
return $methodDef;
}
public function hasAbstractMethod(string $method): bool
{
return isset($this->abstractMethods[strtolower($method)]);

@ -19,6 +19,7 @@ use TypePhp\Entity\MethodDef;
use TypePhp\Entity\PropertyDef;
use TypePhp\Exception\SyntaxError;
use TypePhp\Transform\PropertyHookLowering;
use TypePhp\Transform\PrinterLowering;
use TypePhp\Transform\Visitor;
use PhpParser\Modifiers;
use PhpParser\ConstExprEvaluator;
@ -694,6 +695,25 @@ class Preprocessor extends CompilerBase
}
$this->symbolDeclInFile[$fullClassNameLower] = $this->file;
if ($class instanceof Node\Stmt\Class_) {
$generatedPrinter = false;
foreach ($class->getMethods() as $method) {
if ($method->getAttribute(PrinterLowering::GENERATED_ATTRIBUTE)) {
$generatedPrinter = true;
break;
}
}
if ($generatedPrinter && $this->parentHasMethod($this->classDef->extends, 'toString')) {
PrinterLowering::removeGeneratedMethod($class);
} elseif ($generatedPrinter) {
$this->classDef->printerGenerated = true;
PrinterLowering::rebuildGeneratedMethod(
$class,
[...$this->parentPublicProperties($this->classDef->extends), ...PrinterLowering::ownPublicProperties($class)],
);
}
}
// Property defaults may reference class constants declared later in the
// class body. Collect every constant first so default-value validation
// is independent of declaration order, matching PHP's class semantics.
@ -754,6 +774,64 @@ class Preprocessor extends CompilerBase
return $code;
}
public function shouldGeneratePrinter(string $class): bool
{
$classDef = $this->getClassDef(ltrim($class, '\\'));
if ($classDef === null) {
return true;
}
// A child may be discovered before its parent during the initial
// project scan. Reconcile the provisional method once every class is
// available, before conversion and arginfo generation begin.
if ($classDef->printerGenerated && $this->parentHasMethod($classDef->extends, 'toString')) {
$generated = $classDef->removeMethod('toString');
if ($generated?->functionDef !== null) {
foreach ($this->symbols->functions() as $name => $functionDef) {
if ($functionDef === $generated->functionDef) {
$this->symbols->removeFunction($name);
break;
}
}
}
$classDef->printerGenerated = false;
}
return $classDef->printerGenerated;
}
/** @return list<string> */
protected function parentPublicProperties(string $parent): array
{
if ($parent === '') {
return [];
}
$classDef = $this->getClassDef($parent);
if ($classDef === null) {
return [];
}
$properties = $this->parentPublicProperties($classDef->extends);
foreach ($classDef->properties as $property) {
if ($property->isPublic() && !$property->isStatic()) {
$properties[] = $property->name;
}
}
return array_values(array_unique($properties));
}
protected function parentHasMethod(string $parent, string $method): bool
{
while ($parent !== '') {
$classDef = $this->getClassDef($parent);
if ($classDef === null) {
return $this->isInternalClass($parent) && method_exists($parent, $method);
}
if ($classDef->hasMethod($method) || $classDef->hasAbstractMethod($method)) {
return true;
}
$parent = $classDef->extends;
}
return false;
}
protected function parseExtensionProviderTarget(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): ?string
{
foreach ($class->attrGroups as $groupIndex => $group) {

@ -26,6 +26,7 @@ final class SymbolRepository
public function putFunction(string $key, FunctionDef $definition): void { $this->functions[$key] = $definition; }
public function hasFunction(string $key): bool { return array_key_exists($key, $this->functions); }
public function function(string $key): FunctionDef { return $this->functions[$key]; }
public function removeFunction(string $key): void { unset($this->functions[$key]); }
/** @return array<string, FunctionDef> */
public function functions(): array { return $this->functions; }

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

@ -14,8 +14,28 @@ use PhpParser\NodeVisitorAbstract;
class Visitor extends NodeVisitorAbstract
{
/** @param null|callable(Stmt\Class_): bool $printerPredicate */
public function __construct(private $printerPredicate = null)
{
}
public function enterNode(Node $node): null
{
GetterLowering::validateTarget($node);
PropertyMethodLowering::validateTarget($node);
NotNullLowering::validateTarget($node);
PrinterLowering::validateTarget($node);
return null;
}
public function leaveNode(Node $node): null
{
if ($node instanceof Stmt\Function_ || $node instanceof Stmt\ClassMethod || $node instanceof Node\Expr\Closure) {
NotNullLowering::lowerFunction($node);
} elseif ($node instanceof Node\Expr\ArrowFunction) {
NotNullLowering::rejectArrowFunction($node);
}
if (!$node instanceof Stmt\Class_ && !$node instanceof Stmt\Trait_ && !$node instanceof Stmt\Enum_) {
return null;
}
@ -24,18 +44,31 @@ class Visitor extends NodeVisitorAbstract
foreach ($node->stmts as $stmt) {
if ($stmt instanceof Stmt\Property) {
array_push($methods, ...PropertyHookLowering::lowerProperty($stmt));
array_push($methods, ...GetterLowering::lowerProperty($stmt));
array_push($methods, ...PropertyMethodLowering::lowerProperty($stmt));
} elseif ($stmt instanceof Stmt\ClassMethod && $stmt->name->toLowerString() === '__construct') {
foreach ($stmt->params as $param) {
$marker = PropertyHookLowering::lowerPromotedProperty($param);
if ($marker !== null) {
$methods[] = $marker;
}
$getter = GetterLowering::lowerPromotedProperty($param);
if ($getter !== null) {
$methods[] = $getter;
}
array_push($methods, ...PropertyMethodLowering::lowerPromotedProperty($param));
}
}
}
if ($methods !== []) {
array_push($node->stmts, ...$methods);
}
if ($node instanceof Stmt\Class_) {
if (CompileTimeAttribute::has($node, 'Printer')) {
$generate = $this->printerPredicate === null || ($this->printerPredicate)($node);
PrinterLowering::lowerClass($node, $generate);
}
}
return null;
}
}

@ -2287,7 +2287,10 @@ CODE;
$ast = $this->parser->parse($phpCode);
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new Visitor());
$traverser->addVisitor(new Visitor(function (Node\Stmt\Class_ $class): bool {
$name = isset($class->namespacedName) ? $class->namespacedName->toString() : $class->name->toString();
return $this->shouldGeneratePrinter($name);
}));
$stmts = $traverser->traverse($ast);
@ -2871,6 +2874,13 @@ CODE;
$this->classDef = $this->getClass($fullName);
$this->parseExtensionProviderTarget($class);
if ($class instanceof Node\Stmt\Class_ && $this->classDef->printerGenerated) {
\TypePhp\Transform\PrinterLowering::rebuildGeneratedMethod(
$class,
[...$this->parentPublicProperties($this->classDef->extends), ...\TypePhp\Transform\PrinterLowering::ownPublicProperties($class)],
);
}
// 如果不是继承自内置类,需要检查父类是否存在,在预处理阶段只需检查了是否继承内置类
// 目前不允许继承自动态加载的自定义类
if ($this->classDef->extends and !$this->classDef->inheritedFromInternalClass) {

@ -3584,7 +3584,9 @@ class AttributeInfo {
foreach ($attrGroup->attrs as $attr) {
$parts = $attr->name->getParts();
$compileTimeAttribute = count($parts) === 1
&& in_array(strtolower($parts[0]), ['extensionprovider', 'noexport'], true);
&& in_array(strtolower($parts[0]), [
'extensionprovider', 'getter', 'noexport', 'notnull', 'printer', 'setter', 'with',
], true);
if ($compileTimeAttribute) {
continue;
}
@ -4507,11 +4509,17 @@ class FileInfo {
public static function parseStubFile(string $code, string $phpVersion = '8.5'): FileInfo {
$parser = (new PhpParser\ParserFactory())->createForVersion(PhpParser\PhpVersion::fromString($phpVersion));
$nodeTraverser = new PhpParser\NodeTraverser;
$nodeTraverser->addVisitor(new TypePhp\Transform\Visitor());
$nodeTraverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(
null,
['preserveOriginalNames' => true]
));
$nodeTraverser->addVisitor(new TypePhp\Transform\Visitor(static function (Stmt\Class_ $class): bool {
if (!isset($GLOBALS['translator'])) {
return true;
}
$name = isset($class->namespacedName) ? $class->namespacedName->toString() : $class->name->toString();
return getTranslator()->shouldGeneratePrinter($name);
}));
$prettyPrinter = new class extends Standard {
protected function pName_FullyQualified(PhpParser\Node\Name\FullyQualified $node): string {
return implode('\\', $node->getParts());

@ -19,6 +19,31 @@ final readonly class NoExport
{
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final readonly class Getter
{
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final readonly class Setter
{
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final readonly class With
{
}
#[Attribute(Attribute::TARGET_CLASS)]
final readonly class Printer
{
}
#[Attribute(Attribute::TARGET_PARAMETER)]
final readonly class NotNull
{
}
/**
* Public compile-time type symbols shared by extension providers and std containers.
* This root class is deliberately distinct from the compiler-internal TypePhp\Type.

Loading…
Cancel
Save