From c91f05af39ce595bd5bfbe6a766feef5e42d01b9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Aug 2026 10:51:29 +0200 Subject: [PATCH 1/5] Implement support for XP generics --- src/main/php/lang/Reflection.class.php | 48 +++-- .../php/lang/meta/MetaInformation.class.php | 12 +- .../php/lang/reflection/Constant.class.php | 2 +- .../php/lang/reflection/Constructor.class.php | 11 +- .../php/lang/reflection/GenericType.class.php | 23 +++ src/main/php/lang/reflection/Member.class.php | 39 ++-- src/main/php/lang/reflection/Method.class.php | 66 +++++- .../php/lang/reflection/Parameter.class.php | 32 ++- .../php/lang/reflection/Parameters.class.php | 39 ++-- .../php/lang/reflection/Property.class.php | 2 +- .../php/lang/reflection/Routine.class.php | 9 +- src/main/php/lang/reflection/Type.class.php | 53 ++++- .../xp/reflection/ClassInformation.class.php | 3 +- .../xp/reflection/TypeInformation.class.php | 5 + .../unittest/GenericsTest.class.php | 188 ++++++++++++++++++ .../reflection/unittest/Sequence.class.php | 38 ++++ 16 files changed, 498 insertions(+), 72 deletions(-) create mode 100755 src/main/php/lang/reflection/GenericType.class.php create mode 100755 src/test/php/lang/reflection/unittest/GenericsTest.class.php create mode 100755 src/test/php/lang/reflection/unittest/Sequence.class.php diff --git a/src/main/php/lang/Reflection.class.php b/src/main/php/lang/Reflection.class.php index 7c70851..ef542b9 100755 --- a/src/main/php/lang/Reflection.class.php +++ b/src/main/php/lang/Reflection.class.php @@ -1,7 +1,7 @@ reflect()); + $reflect= $arg->reflect(); } else if ($arg instanceof \ReflectionClass) { - return new Type($arg); + $reflect= $arg; } else if ($arg instanceof Type) { return $arg; } else if (is_object($arg)) { - return new Type(new \ReflectionObject($arg)); + $reflect= new \ReflectionObject($arg); } else { try { - return new Type(new \ReflectionClass(strtr($arg, '.', '\\'))); + $reflect= new \ReflectionClass(strtr($arg, '.', '\\')); } catch (\ReflectionException $e) { throw new ClassNotFoundException($arg, [ClassLoader::getDefault()]); } } + + return strpos($reflect->name, "\xb7\xb7") ? new GenericType($reflect) : new Type($reflect); } /** @@ -94,28 +96,30 @@ public static function package($arg) { * @throws lang.ClassNotFoundException */ public static function of($arg) { - if ($arg instanceof XPClass) { - return new Type($arg->reflect()); - } else if ($arg instanceof \ReflectionClass) { - return new Type($arg); - } else if ($arg instanceof Type) { + if ($arg instanceof Type) { return $arg; + } else if ($arg instanceof XPClass) { + $reflect= $arg->reflect(); + } else if ($arg instanceof \ReflectionClass) { + $reflect= $arg; } else if (is_object($arg)) { - return new Type(new \ReflectionObject($arg)); + $reflect= new \ReflectionObject($arg); } else { $cl= ClassLoader::getDefault(); $name= strtr($arg, '\\', '.'); if ($cl->providesClass($name)) { - return new Type(new \ReflectionClass($cl->loadClass0($name))); + $reflect= new \ReflectionClass($cl->loadClass0($name)); } else if ($cl->providesPackage($name)) { return new Package($name); - } - - try { - return new Type(new \ReflectionClass(strtr($arg, '.', '\\'))); - } catch (\ReflectionException $e) { - throw new ClassNotFoundException($name, [$cl]); + } else { + try { + $reflect= new \ReflectionClass(strtr($arg, '.', '\\')); + } catch (\ReflectionException $e) { + throw new ClassNotFoundException($name, [$cl]); + } } } + + return strpos($reflect->name, "\xb7\xb7") ? new GenericType($reflect) : new Type($reflect); } } \ No newline at end of file diff --git a/src/main/php/lang/meta/MetaInformation.class.php b/src/main/php/lang/meta/MetaInformation.class.php index 57f792c..f5cfd78 100755 --- a/src/main/php/lang/meta/MetaInformation.class.php +++ b/src/main/php/lang/meta/MetaInformation.class.php @@ -25,7 +25,7 @@ public function evaluate($reflect, $code) { */ private function annotations($meta) { $r= []; - foreach ($meta[DETAIL_ANNOTATIONS] as $name => $value) { + foreach ($meta[DETAIL_ANNOTATIONS] ?? [] as $name => $value) { $qname= $meta[DETAIL_TARGET_ANNO][$name] ?? $name; $r[$qname]= isset($meta[DETAIL_TARGET_ANNO][$qname]) ? [$value] : (array)$value; } @@ -72,6 +72,16 @@ public function typeAnnotations($reflect) { } } + /** + * Returns type generics for a given type, if any + * + * @param \ReflectionClass $reflect + * @return ?var[] + */ + public function typeGenerics($reflect) { + return \xp::$meta[\xp::$cn[$reflect->name] ?? strtr($reflect->name, '\\', '.')]['class'][DETAIL_GENERIC] ?? null; + } + /** * Returns API doc comment for a given type * diff --git a/src/main/php/lang/reflection/Constant.class.php b/src/main/php/lang/reflection/Constant.class.php index 87a7d92..1a76060 100755 --- a/src/main/php/lang/reflection/Constant.class.php +++ b/src/main/php/lang/reflection/Constant.class.php @@ -34,7 +34,7 @@ public function constraint() { $t= Type::resolve( PHP_VERSION_ID >= 80300 ? $this->reflect->getType() : null, - Member::resolve($this->reflect), + $this->resolve(), $api ); return new Constraint($t ?? Type::$VAR, $present); diff --git a/src/main/php/lang/reflection/Constructor.class.php b/src/main/php/lang/reflection/Constructor.class.php index b1665fb..5fd81c7 100755 --- a/src/main/php/lang/reflection/Constructor.class.php +++ b/src/main/php/lang/reflection/Constructor.class.php @@ -1,7 +1,7 @@ methodAnnotations($this->reflect)[Generic::class] ?? []) { + $params= [...Type::split($generic['params'] ?? '')]; + } else { + $params= []; + } + return Modifiers::namesOf($this->reflect->getModifiers() & ~0x1fb7f008). - ' function __construct('.$this->signature(Reflection::meta()).')' + ' function __construct('.$this->signature($meta, $params).')' ; } diff --git a/src/main/php/lang/reflection/GenericType.class.php b/src/main/php/lang/reflection/GenericType.class.php new file mode 100755 index 0000000..9d0bc60 --- /dev/null +++ b/src/main/php/lang/reflection/GenericType.class.php @@ -0,0 +1,23 @@ +generics??= Reflection::meta()->typeGenerics($this->reflect); + return new parent(new ReflectionClass(strtr($this->generics[0], '.', '\\'))); + } + + /** @return lang.Type[] */ + public function arguments(): array { + $this->generics??= Reflection::meta()->typeGenerics($this->reflect); + return $this->generics[1]; + } +} \ No newline at end of file diff --git a/src/main/php/lang/reflection/Member.class.php b/src/main/php/lang/reflection/Member.class.php index 568b394..f59b697 100755 --- a/src/main/php/lang/reflection/Member.class.php +++ b/src/main/php/lang/reflection/Member.class.php @@ -18,23 +18,38 @@ public function __construct($reflect, $annotations= null) { $this->annotations= $annotations; } + /** Returns definition for a given generic class */ + protected function definitionOf($class) { + return substr($class, 0, strpos($class, "\xb7\xb7")); + } + /** * Returns context for `Type::resolve()` * - * @param ReflectionMethod|ReflectionProperty|ReflectionClassConstant $reflect * @return [:function(?string): Type] */ - public static function resolve($reflect) { - return [ - 'static' => fn() => new XPClass($reflect->class), - 'self' => fn() => new XPClass($reflect->getDeclaringClass()), - 'parent' => fn() => new XPClass($reflect->getDeclaringClass()->getParentClass()), - '*' => function($type) use($reflect) { - $declared= $reflect->getDeclaringClass(); - $imports= Reflection::meta()->scopeImports($declared); - return XPClass::forName($imports[$type] ?? $declared->getNamespaceName().'\\'.$type); - }, - ]; + public function resolve() { + $declared= $this->reflect->getDeclaringClass(); + + // Inside `Type`, `self` should resolve to the base type + if (strpos($this->reflect->class, "\xb7\xb7")) { + $resolve= [ + 'static' => fn() => new XPClass($this->definitionOf($this->reflect->class)), + 'self' => fn() => new XPClass($this->definitionOf($declared->name)), + 'parent' => fn() => new XPClass(get_parent_class($this->definitionOf($declared->name))), + ]; + } else { + $resolve= [ + 'static' => fn() => new XPClass($this->reflect->class), + 'self' => fn() => new XPClass($declared), + 'parent' => fn() => new XPClass($declared->getParentClass()), + ]; + } + + return $resolve + ['*' => function($type) use($declared) { + $imports= Reflection::meta()->scopeImports($declared); + return XPClass::forName($imports[$type] ?? $declared->getNamespaceName().'\\'.$type); + }]; } /** @return [:var] */ diff --git a/src/main/php/lang/reflection/Method.class.php b/src/main/php/lang/reflection/Method.class.php index cfe43da..b3ea009 100755 --- a/src/main/php/lang/reflection/Method.class.php +++ b/src/main/php/lang/reflection/Method.class.php @@ -1,7 +1,16 @@ methodAnnotations($this->reflect)[Generic::class]['self'] ?? null; + return $generic ? [...XPClass::split($generic)] : null; + } + + /** + * Parameterizes this method with type arguments + * + * @param lang.Type[] $arguments + * @return lang.reflection.GenericMethod + * @throws lang.IllegalStateException if this type is not generic + * @throws lang.IllegalArgumentException for incorrect numbers of type parameters + */ + public function parameterize(array $arguments): GenericMethod { + $generic= $this->parameterized(); + + if (null === $generic) { + throw new IllegalStateException('Method '.$this->name().' is not generic'); + } else if (sizeof($arguments) !== sizeof($generic)) { + throw new IllegalArgumentException('Expected '.sizeof($generic).' argument(s), have '.sizeof($arguments)); + } + + return new GenericMethod($this->reflect, $arguments); + } + + /** + * Returns context for `Type::resolve()` + * + * @return [:function(?string): Type] + */ + public function resolve() { + $resolve= parent::resolve(); + foreach ($this->parameterized() ?? [] as $param) { + $resolve[$param]= fn() => new TypeParameter($param); + } + return $resolve; + } + /** * Returns a closure * @@ -79,19 +131,24 @@ public function returns() { return Reflection::meta()->methodReturns($this->reflect); }; - $t= Type::resolve($this->reflect->getReturnType(), Member::resolve($this->reflect), $api); + $t= Type::resolve($this->reflect->getReturnType(), $this->resolve(), $api); return new Constraint($t ?? Type::$VAR, $present); } /** @return string */ public function toString() { $meta= Reflection::meta(); + if ($generic= $meta->methodAnnotations($this->reflect)[Generic::class] ?? []) { + $params= [...Type::split($generic['params'] ?? '')]; + } else { + $params= []; + } // Put together return type $t= $this->reflect->getReturnType(); $nullable= ''; if (null === $t) { - $returns= $meta->methodReturns($this->reflect) ?? 'var'; + $returns= $meta->methodReturns($this->reflect) ?? $generic['return'] ?? 'var'; } else if ($t instanceof ReflectionUnionType) { $name= ''; foreach ($t->getTypes() as $component) { @@ -113,9 +170,10 @@ public function toString() { $t->allowsNull() && $nullable= '?'; } + $parameterized= isset($generic['self']) ? '<'.$generic['self'].'>' : ''; return Modifiers::namesOf($this->reflect->getModifiers() & ~0x1fb7f008). - ' function '.$this->reflect->name.'('.$this->signature($meta).'): '. + ' function '.$this->reflect->name.$parameterized.'('.$this->signature($meta, $params).'): '. $nullable.$returns ; } diff --git a/src/main/php/lang/reflection/Parameter.class.php b/src/main/php/lang/reflection/Parameter.class.php index cb11475..9728a02 100755 --- a/src/main/php/lang/reflection/Parameter.class.php +++ b/src/main/php/lang/reflection/Parameter.class.php @@ -1,24 +1,26 @@ reflect= $reflect; + $this->resolve= $resolve; $this->method= $method ?? $reflect->getDeclaringFunction(); } @@ -80,8 +82,28 @@ public function constraint() { }; return new Constraint( - Type::resolve($this->reflect->getType(), Member::resolve($this->reflect), $api) ?? Type::$VAR, + Type::resolve($this->reflect->getType(), $this->resolve, $api) ?? Type::$VAR, $present ); } + + /** @return string */ + public function toString() { + return nameof($this).'<'.$this->reflect->name.'>'; + } + + /** @return string */ + public function hashCode() { + return 'P'.Objects::hashOf([$this->method->name, $this->reflect->name]); + } + + /** + * Comparison + * + * @param var $value + * @return int + */ + public function compareTo($value) { + return $value instanceof self ? $this->reflect <=> $value->reflect : 1; + } } \ No newline at end of file diff --git a/src/main/php/lang/reflection/Parameters.class.php b/src/main/php/lang/reflection/Parameters.class.php index 6854099..971f428 100755 --- a/src/main/php/lang/reflection/Parameters.class.php +++ b/src/main/php/lang/reflection/Parameters.class.php @@ -9,11 +9,17 @@ * @test lang.reflection.unittest.MethodsTest */ class Parameters implements \IteratorAggregate { - private $method; + private $reflect, $resolve; - /** @param ReflectionMethod $method */ - public function __construct($method) { - $this->method= $method; + /** + * Creates a new instance + * + * @param ReflectionMethod $reflect + * @param [:function(?string): Type] $resolve + */ + public function __construct($reflect, $resolve) { + $this->reflect= $reflect; + $this->resolve= $resolve; } /** @@ -23,7 +29,7 @@ public function __construct($method) { * @return int */ public function size($required= false) { - return $required ? $this->method->getNumberOfRequiredParameters() : $this->method->getNumberOfParameters(); + return $required ? $this->reflect->getNumberOfRequiredParameters() : $this->reflect->getNumberOfParameters(); } /** @@ -33,8 +39,8 @@ public function size($required= false) { * @return ?lang.reflection.Parameter */ public function at(int $position) { - $list= $this->method->getParameters(); - return isset($list[$position]) ? new Parameter($list[$position], $this->method) : null; + $list= $this->reflect->getParameters(); + return isset($list[$position]) ? new Parameter($list[$position], $this->resolve, $this->reflect) : null; } /** @@ -44,22 +50,22 @@ public function at(int $position) { * @return ?lang.reflection.Parameter */ public function named(string $name) { - foreach ($this->method->getParameters() as $param) { - if ($name === $param->name) return new Parameter($param, $this->method); + foreach ($this->reflect->getParameters() as $param) { + if ($name === $param->name) return new Parameter($param, $this->resolve, $this->reflect); } return null; } /** @return ?lang.reflection.Parameter */ public function first() { - $list= $this->method->getParameters(); - return $list ? new Parameter($list[0], $this->method) : null; + $list= $this->reflect->getParameters(); + return $list ? new Parameter($list[0], $this->resolve, $this->reflect) : null; } /** @return iterable */ public function getIterator(): Traversable { - foreach ($this->method->getParameters() as $parameter) { - yield $parameter->name => new Parameter($parameter, $this->method); + foreach ($this->reflect->getParameters() as $parameter) { + yield $parameter->name => new Parameter($parameter, $this->resolve, $this->reflect); } } @@ -73,23 +79,22 @@ public function getIterator(): Traversable { * @return bool */ public function accept(array $arguments, $count= null): bool { - $parameters= $this->method->getParameters(); + $parameters= $this->reflect->getParameters(); if (null !== $count && $count !== sizeof($parameters)) return false; // Only fetch api doc types if necessary $api= function() use(&$i, &$types) { - $types ?? $types= Reflection::meta()->methodParameterTypes($this->method); + $types ?? $types= Reflection::meta()->methodParameterTypes($this->reflect); return $types[$i] ?? null; }; - $context= Member::resolve($this->method); foreach ($parameters as $i => $parameter) { // If a given value is missing check whether parameter is optional if (!array_key_exists($i, $arguments)) return $parameter->isOptional(); // A value is present for this parameter, now check type - if (null === ($type= Type::resolve($parameter->getType(), $context, $api))) continue; + if (null === ($type= Type::resolve($parameter->getType(), $this->resolve, $api))) continue; // For variadic parameters, verify rest of arguments if ($parameter->isVariadic()) { diff --git a/src/main/php/lang/reflection/Property.class.php b/src/main/php/lang/reflection/Property.class.php index 6b8358f..fc8b9a2 100755 --- a/src/main/php/lang/reflection/Property.class.php +++ b/src/main/php/lang/reflection/Property.class.php @@ -34,7 +34,7 @@ public function constraint() { return Reflection::meta()->propertyType($this->reflect); }; - $t= Type::resolve($this->reflect->getType(), Member::resolve($this->reflect), $api); + $t= Type::resolve($this->reflect->getType(), $this->resolve(), $api); return new Constraint($t ?? Type::$VAR, $present); } diff --git a/src/main/php/lang/reflection/Routine.class.php b/src/main/php/lang/reflection/Routine.class.php index 535567c..a867d7f 100755 --- a/src/main/php/lang/reflection/Routine.class.php +++ b/src/main/php/lang/reflection/Routine.class.php @@ -17,16 +17,17 @@ protected function meta() { return Reflection::meta()->methodAnnotations($this-> * Compiles signature * * @param lang.meta.MetaInformation $meta + * @param string[] $params * @return string */ - protected function signature($meta) { + protected function signature($meta, $params= []) { $types= $meta->methodParameterTypes($this->reflect); $r= ''; foreach ($this->reflect->getParameters() as $i => $parameter) { $t= $parameter->getType(); $nullable= ''; if (null === $t) { - $type= $types[$i] ?? ($parameter->isVariadic() ? 'var...' : 'var'); + $type= $types[$i] ?? $params[$i] ?? ($parameter->isVariadic() ? 'var...' : 'var'); } else if ($t instanceof ReflectionUnionType) { $name= ''; foreach ($t->getTypes() as $component) { @@ -80,7 +81,7 @@ public function parameter($arg) { break; } } - return null === $p ? null : new Parameter($p, $this->reflect); + return null === $p ? null : new Parameter($p, $this->resolve(), $this->reflect); } /** @@ -89,7 +90,7 @@ public function parameter($arg) { * @return lang.reflection.Parameters */ public function parameters(): Parameters { - return new Parameters($this->reflect); + return new Parameters($this->reflect, $this->resolve()); } /** Support named arguments for PHP 7.X */ diff --git a/src/main/php/lang/reflection/Type.class.php b/src/main/php/lang/reflection/Type.class.php index e8b9427..f7aec17 100755 --- a/src/main/php/lang/reflection/Type.class.php +++ b/src/main/php/lang/reflection/Type.class.php @@ -1,15 +1,26 @@ annotations ?? $this->annotations= Reflection::meta()->typeAnnotations($this->reflect); + if ($generic= $this->annotations[Generic::class]['self'] ?? null) { + return [...XPClass::split($generic)]; + } + return null; + } + + /** + * Parameterizes this type with type arguments + * + * @param lang.Type[] $arguments + * @return lang.reflection.GenericType + * @throws lang.IllegalStateException if this type is not generic + * @throws lang.IllegalArgumentException for incorrect numbers of type parameters + */ + public function parameterize(array $arguments): GenericType { + static $types= null; + + $generic= $this->parameterized(); + if (null === $generic) { + throw new IllegalStateException('Type '.$this->name().' is not generic'); + } else if (sizeof($arguments) !== sizeof($generic)) { + throw new IllegalArgumentException('Expected '.sizeof($generic).' argument(s), have '.sizeof($arguments)); + } + + $types??= new GenericTypes(); + return new GenericType(new ReflectionClass($types->newType0(new XPClass($this->reflect), $arguments))); + } + + /** Returns whether this type is generic */ + public function generic() { return false; } + /** @return ?lang.IClassLoader */ public function classLoader() { $name= strtr($this->reflect->name, '\\', '.'); diff --git a/src/main/php/xp/reflection/ClassInformation.class.php b/src/main/php/xp/reflection/ClassInformation.class.php index 20c290a..be3e603 100755 --- a/src/main/php/xp/reflection/ClassInformation.class.php +++ b/src/main/php/xp/reflection/ClassInformation.class.php @@ -5,9 +5,10 @@ class ClassInformation extends TypeInformation { public function display($out) { $this->documentation($out, $this->type); $out->format( - '%s class %s%s%s {', + '%s class %s%s%s%s {', $this->type->modifiers(), $this->type->name(), + $this->parameterized($this->type), $this->extends($this->type), $this->implements($this->type) ); diff --git a/src/main/php/xp/reflection/TypeInformation.class.php b/src/main/php/xp/reflection/TypeInformation.class.php index 7b1898a..9e51e51 100755 --- a/src/main/php/xp/reflection/TypeInformation.class.php +++ b/src/main/php/xp/reflection/TypeInformation.class.php @@ -61,6 +61,11 @@ protected function parents($type) { return $i ? ' extends '.implode(', ', array_map(fn($t) => $t->name(), $i)) : ''; } + protected function parameterized($type) { + $c= $type->parameterized(); + return $c ? '<'.implode(', ', $c).'>' : ''; + } + protected function partition($members) { $r= ['class' => [], 'instance' => []]; foreach ($members as $member) { diff --git a/src/test/php/lang/reflection/unittest/GenericsTest.class.php b/src/test/php/lang/reflection/unittest/GenericsTest.class.php new file mode 100755 index 0000000..73c4e46 --- /dev/null +++ b/src/test/php/lang/reflection/unittest/GenericsTest.class.php @@ -0,0 +1,188 @@ +sequence, true]; + } + + /** @return iterable */ + private function types() { + yield [Reflection::of(self::class), null]; + yield [Reflection::of(Sequence::class), ['T']]; + yield [Reflection::of($this->sequence), null]; + } + + /** @return iterable */ + private function methods() { + yield [Reflection::of(self::class)->method(__FUNCTION__), null]; + yield [Reflection::of(Sequence::class)->method('toString'), null]; + yield [Reflection::of($this->sequence)->method('map'), ['R']]; + } + + /** @return iterable */ + private function incorrect() { + yield [[], 'no arguments']; + yield [[Primitive::$INT, Primitive::$STRING], 'too many arguments']; + } + + #[Before] + public function sequence() { + $this->sequence= create('new lang.reflection.unittest.Sequence', 'Hello', 'Test'); + } + + #[Test, Values(from: 'generic')] + public function reflection_of($arg, $expected) { + Assert::equals($expected, Reflection::of($arg)->generic()); + } + + #[Test, Values(from: 'generic')] + public function reflection_type($arg, $expected) { + Assert::equals($expected, Reflection::type($arg)->generic()); + } + + #[Test, Values(from: 'types')] + public function parameterized_type($type, $expected) { + Assert::equals($expected, $type->parameterized()); + } + + #[Test, Values(from: 'methods')] + public function parameterized_method($method, $expected) { + Assert::equals($expected, $method->parameterized()); + } + + #[Test] + public function parameterize_type() { + $definition= Reflection::type(Sequence::class); + $t= $definition->parameterize([Primitive::$STRING]); + + Assert::true($t->generic()); + Assert::equals($definition, $t->definition()); + Assert::equals([Primitive::$STRING], $t->arguments()); + } + + #[Test] + public function parameterize_method() { + $definition= Reflection::type($this->sequence)->method('map'); + $m= $definition->parameterize([Primitive::$INT]); + + Assert::true($m->generic()); + Assert::equals($definition, $m->definition()); + Assert::equals([Primitive::$INT], $m->arguments()); + } + + #[Test, Expect(IllegalStateException::class)] + public function parameterize_non_generic_type() { + Reflection::type(self::class)->parameterize([]); + } + + #[Test, Expect(IllegalStateException::class)] + public function parameterize_non_generic_method() { + Reflection::type($this->sequence)->method('elements')->parameterize([]); + } + + #[Test, Expect(IllegalArgumentException::class), Values(from: 'incorrect')] + public function parameterize_type_with_incorrect($arguments) { + Reflection::type(Sequence::class)->parameterize($arguments); + } + + #[Test, Expect(IllegalArgumentException::class), Values(from: 'incorrect')] + public function parameterize_method_with_incorrect($arguments) { + Reflection::type($this->sequence)->method('map')->parameterize($arguments); + } + + #[Test] + public function constructor_parameter_resolved() { + Assert::equals( + Type::forName('string[]'), + Reflection::type($this->sequence)->constructor()->parameter(0)->constraint()->type() + ); + } + + #[Test] + public function method_parameter_resolved() { + Assert::equals( + Type::forName('string[]'), + Reflection::type($this->sequence)->method('extend')->parameter(0)->constraint()->type() + ); + } + + #[Test] + public function method_returns_component_resolved() { + Assert::equals( + Type::forName('string[]'), + Reflection::type($this->sequence)->method('elements')->returns()->type() + ); + } + + #[Test] + public function method_returns_self_resolved() { + $type= Reflection::type($this->sequence); + Assert::equals( + $type->class(), + $type->method('extend')->returns()->type() + ); + } + + #[Test] + public function generic_method_returns() { + $method= Reflection::type($this->sequence)->method('map'); + Assert::equals( + Type::named('lang.reflection.unittest.Sequence', ['R' => fn() => new TypeParameter('R')]), + $method->returns()->type() + ); + } + + #[Test] + public function generic_method_parameter() { + $method= Reflection::type($this->sequence)->method('map'); + Assert::equals( + Type::named('function(string): R', ['R' => fn() => new TypeParameter('R')]), + $method->parameter(0)->constraint()->type() + ); + } + + #[Test] + public function generic_method_returns_resolved() { + $method= Reflection::type($this->sequence)->method('map'); + Assert::equals( + Type::forName('lang.reflection.unittest.Sequence'), + $method->parameterize([Primitive::$INT])->returns()->type() + ); + } + + #[Test] + public function generic_method_parameter_resolved() { + $method= Reflection::type($this->sequence)->method('map'); + Assert::equals( + Type::forName('function(string): int'), + $method->parameterize([Primitive::$INT])->parameter(0)->constraint()->type() + ); + } + + #[Test] + public function generic_method_invocation() { + $method= Reflection::type($this->sequence) + ->method('map') + ->parameterize([Primitive::$INT]) + ; + Assert::equals([5, 4], $method->invoke($this->sequence, [fn($e) => strlen($e)])->elements()); + } + + #[Test] + public function generic_method_closure() { + $closure= Reflection::type($this->sequence) + ->method('map') + ->parameterize([Primitive::$INT]) + ->closure($this->sequence) + ; + Assert::equals([5, 4], $closure(fn($e) => strlen($e))->elements()); + } +} \ No newline at end of file diff --git a/src/test/php/lang/reflection/unittest/Sequence.class.php b/src/test/php/lang/reflection/unittest/Sequence.class.php new file mode 100755 index 0000000..7ec044c --- /dev/null +++ b/src/test/php/lang/reflection/unittest/Sequence.class.php @@ -0,0 +1,38 @@ +elements= $elements; + } + + #[Generic(params: 'T[]', return: 'self')] + public function extend($elements): self { + foreach ($elements as $element) { + $this->elements[]= $element; + } + return $this; + } + + #[Generic(self: 'R', params: 'function(T): R', return: 'self')] + public function map($map) { + return create("new self<$R>")->extend(array_map($map, $this->elements)); + } + + #[Generic(return: 'T[]')] + public function elements() { return $this->elements; } + + public function hashCode() { return 'S'.Objects::hashOf($this->elements); } + + public function toString() { return nameof($this).'@'.Objects::stringOf($this->elements); } + + public function compareTo($value) { + return $value instanceof self ? $this->elements <=> $value->elements : 1; + } +} \ No newline at end of file From 148cd754bbaab7d16daf22c77c05c4d14b5d00f9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 22 Aug 2026 11:15:21 +0200 Subject: [PATCH 2/5] Fix meta annotation lookups --- src/main/php/lang/meta/MetaInformation.class.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/meta/MetaInformation.class.php b/src/main/php/lang/meta/MetaInformation.class.php index f5cfd78..b64e04a 100755 --- a/src/main/php/lang/meta/MetaInformation.class.php +++ b/src/main/php/lang/meta/MetaInformation.class.php @@ -27,7 +27,7 @@ private function annotations($meta) { $r= []; foreach ($meta[DETAIL_ANNOTATIONS] ?? [] as $name => $value) { $qname= $meta[DETAIL_TARGET_ANNO][$name] ?? $name; - $r[$qname]= isset($meta[DETAIL_TARGET_ANNO][$qname]) ? [$value] : (array)$value; + $r[strtr($qname, '.', '\\')]= isset($meta[DETAIL_TARGET_ANNO][$qname]) ? [$value] : (array)$value; } return $r; } @@ -226,7 +226,8 @@ public function propertyModifiers($reflect) { * @return [:var[]] */ public function methodAnnotations($reflect) { - $c= strtr($reflect->getDeclaringClass()->name, '\\', '.'); + $name= $reflect->getDeclaringClass()->name; + $c= \xp::$cn[$name] ?? strtr($name, '\\', '.'); if ($meta= \xp::$meta[$c][1][$reflect->name] ?? null) { return $this->annotations($meta); } else { From d9344e1351115f80f5b38d03be7ec0b589ab4da5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 22 Aug 2026 11:25:47 +0200 Subject: [PATCH 3/5] Require XP 12.13+ for generic methods --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 401054c..0e49622 100755 --- a/composer.json +++ b/composer.json @@ -6,8 +6,8 @@ "description" : "Reflection", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^12.0 | ^11.0 | ^10.13", - "xp-framework/ast": "^13.0 | ^12.0 | ^11.0 | ^10.0 | ^9.0 | ^8.0 | ^7.6", + "xp-framework/core": "^12.13", + "xp-framework/ast": "^13.0 | ^12.0", "php" : ">=7.4.0" }, "require-dev" : { From bf42227aa3922b5437d6af01c00e8760f9189f6f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 22 Aug 2026 11:27:05 +0200 Subject: [PATCH 4/5] Add GenericMethod class --- .../lang/reflection/GenericMethod.class.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100755 src/main/php/lang/reflection/GenericMethod.class.php diff --git a/src/main/php/lang/reflection/GenericMethod.class.php b/src/main/php/lang/reflection/GenericMethod.class.php new file mode 100755 index 0000000..e5d01eb --- /dev/null +++ b/src/main/php/lang/reflection/GenericMethod.class.php @@ -0,0 +1,71 @@ +arguments= $arguments; + } + + /** Returns whether this type is generic */ + public function generic() { return true; } + + /** @return lang.reflection.Method */ + public function definition() { return new Method($this->reflect); } + + /** @return lang.Type[] */ + public function arguments() { return $this->arguments; } + + /** + * Returns context for `Type::resolve()` + * + * @return [:function(?string): Type] + */ + public function resolve() { + $declared= $this->reflect->getDeclaringClass(); + $resolve= [ + 'static' => fn() => new XPClass($this->definitionOf($this->reflect->class)), + 'self' => fn() => new XPClass($this->definitionOf($declared->name)), + 'parent' => fn() => new XPClass(get_parent_class($this->definitionOf($declared->name))), + '*' => function($type) use($declared) { + $imports= Reflection::meta()->scopeImports($declared); + return XPClass::forName($imports[$type] ?? $declared->getNamespaceName().'\\'.$type); + }, + ]; + + // Add generic type parameters + $generic= Reflection::meta()->methodAnnotations($this->reflect)[Generic::class]['self']; + foreach (Type::split($generic) as $p => $arg) { + $resolve[$arg]= fn() => $this->arguments[$p]; + } + return $resolve; + } + + /** + * Returns a closure + * + * @param ?object $instance + * @return Closure + * @throws lang.IllegalArgumentException for incorrect or missing instances + */ + public function closure(?object $instance= null) { + return fn(... $args) => parent::closure($instance)($this->arguments, ...$args); + } + + /** + * Invokes this method + * + * @param ?object $instance + * @param var[] $args + * @return var + * @throws lang.reflection.CannotInvoke if prerequisites to the invocation fail + * @throws lang.reflection.InvocationFailed if invocation raises an exception + */ + public function invoke(?object $instance, $args= []) { + return parent::invoke($instance, [$this->arguments, ...$args]); + } +} \ No newline at end of file From 3f511247634c1f375eacb97a2446532b82a7c0ed Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 22 Aug 2026 21:02:45 +0200 Subject: [PATCH 5/5] Add Method::generic() --- src/main/php/lang/reflection/GenericMethod.class.php | 2 +- src/main/php/lang/reflection/Method.class.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/reflection/GenericMethod.class.php b/src/main/php/lang/reflection/GenericMethod.class.php index e5d01eb..7eb8a69 100755 --- a/src/main/php/lang/reflection/GenericMethod.class.php +++ b/src/main/php/lang/reflection/GenericMethod.class.php @@ -11,7 +11,7 @@ public function __construct($reflect, array $arguments) { $this->arguments= $arguments; } - /** Returns whether this type is generic */ + /** Returns whether this method is generic */ public function generic() { return true; } /** @return lang.reflection.Method */ diff --git a/src/main/php/lang/reflection/Method.class.php b/src/main/php/lang/reflection/Method.class.php index b3ea009..8e0f934 100755 --- a/src/main/php/lang/reflection/Method.class.php +++ b/src/main/php/lang/reflection/Method.class.php @@ -20,6 +20,9 @@ */ class Method extends Routine { + /** Returns whether this method is generic */ + public function generic() { return false; } + /** * Returns generic type parameters if this method is parameterized, NULL otherwise *