diff --git a/phpunit/code/compile_time_attributes.php b/phpunit/code/compile_time_attributes.php new file mode 100644 index 00000000..abf4459a --- /dev/null +++ b/phpunit/code/compile_time_attributes.php @@ -0,0 +1,95 @@ +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(); +} diff --git a/phpunit/code/compiler_api/library_import_php.php b/phpunit/code/compiler_api/library_import_php.php index 3a1ce786..ad74a2dc 100644 --- a/phpunit/code/compiler_api/library_import_php.php +++ b/phpunit/code/compiler_api/library_import_php.php @@ -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 { diff --git a/phpunit/code/getter-function.php b/phpunit/code/getter-function.php new file mode 100644 index 00000000..b39eb4b9 --- /dev/null +++ b/phpunit/code/getter-function.php @@ -0,0 +1,7 @@ +getName()); + var_dump($user->getAge()); + var_dump($user->getTitle()); + var_dump($user->getX()); + var_dump($user->getY()); + var_dump($user->getActive()); +} diff --git a/phpunit/code/not-null-invalid-target.php b/phpunit/code/not-null-invalid-target.php new file mode 100644 index 00000000..444bb500 --- /dev/null +++ b/phpunit/code/not-null-invalid-target.php @@ -0,0 +1,6 @@ +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'); diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 2d3c3017..99f06401 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.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()); diff --git a/src/Entity/ClassDef.php b/src/Entity/ClassDef.php index ae6f4649..e43e7216 100644 --- a/src/Entity/ClassDef.php +++ b/src/Entity/ClassDef.php @@ -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)]); diff --git a/src/Preprocessor.php b/src/Preprocessor.php index c94bde60..03eedbdb 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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 */ + 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) { diff --git a/src/Symbol/SymbolRepository.php b/src/Symbol/SymbolRepository.php index 7001b85a..d86c5d38 100644 --- a/src/Symbol/SymbolRepository.php +++ b/src/Symbol/SymbolRepository.php @@ -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 */ public function functions(): array { return $this->functions; } diff --git a/src/Transform/CompileTimeAttribute.php b/src/Transform/CompileTimeAttribute.php new file mode 100644 index 00000000..f962066a --- /dev/null +++ b/src/Transform/CompileTimeAttribute.php @@ -0,0 +1,68 @@ +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'); + } + } +} diff --git a/src/Transform/GetterLowering.php b/src/Transform/GetterLowering.php new file mode 100644 index 00000000..cdaf5601 --- /dev/null +++ b/src/Transform/GetterLowering.php @@ -0,0 +1,79 @@ +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 */ + 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); + } + +} diff --git a/src/Transform/NotNullLowering.php b/src/Transform/NotNullLowering.php new file mode 100644 index 00000000..a1d41b29 --- /dev/null +++ b/src/Transform/NotNullLowering.php @@ -0,0 +1,57 @@ +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'); + } + } + } +} diff --git a/src/Transform/PrinterLowering.php b/src/Transform/PrinterLowering.php new file mode 100644 index 00000000..da21d915 --- /dev/null +++ b/src/Transform/PrinterLowering.php @@ -0,0 +1,103 @@ +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 $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 */ + 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 $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; + } +} diff --git a/src/Transform/PropertyMethodLowering.php b/src/Transform/PropertyMethodLowering.php new file mode 100644 index 00000000..2b921624 --- /dev/null +++ b/src/Transform/PropertyMethodLowering.php @@ -0,0 +1,114 @@ +isStatic()) { + continue; + } + if ($node instanceof Param && $node->isPromoted()) { + continue; + } + throw new SyntaxError($attribute . ' can only be applied to instance properties'); + } + } + + /** @return list */ + 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 */ + 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); + } +} diff --git a/src/Transform/Visitor.php b/src/Transform/Visitor.php index 711796c4..f61131c1 100644 --- a/src/Transform/Visitor.php +++ b/src/Transform/Visitor.php @@ -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; } } diff --git a/src/Translator.php b/src/Translator.php index ecce536d..f1b8ce3e 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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) { diff --git a/src/gen_stub.php b/src/gen_stub.php index cc408c51..e77a3b58 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -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()); diff --git a/src/polyfills.php b/src/polyfills.php index 1b823c52..c3d37e51 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -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.