diff --git a/src/main/php/lang/Reflection.class.php b/src/main/php/lang/Reflection.class.php index 7c70851..392c980 100755 --- a/src/main/php/lang/Reflection.class.php +++ b/src/main/php/lang/Reflection.class.php @@ -1,5 +1,6 @@ providesClass($name)) { - return new Type(new \ReflectionClass($cl->loadClass0($name))); + return new Type(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) { + return new Type(new ReflectionClass(strtr($arg, '.', '\\'))); + } catch (ReflectionException $e) { throw new ClassNotFoundException($name, [$cl]); } } diff --git a/src/main/php/lang/meta/FromAttributes.class.php b/src/main/php/lang/meta/FromAttributes.class.php index 4c77402..c973d7b 100755 --- a/src/main/php/lang/meta/FromAttributes.class.php +++ b/src/main/php/lang/meta/FromAttributes.class.php @@ -30,6 +30,11 @@ private function annotations($reflect, $context) { return $r; } + /** @return iterable */ + public function ofInvocable($reflect) { + return $this->annotations($reflect, $reflect); + } + /** @return iterable */ public function ofType($reflect) { return $this->annotations($reflect, $reflect); diff --git a/src/main/php/lang/meta/FromSyntaxTree.class.php b/src/main/php/lang/meta/FromSyntaxTree.class.php index 6a1b0a5..7892d40 100755 --- a/src/main/php/lang/meta/FromSyntaxTree.class.php +++ b/src/main/php/lang/meta/FromSyntaxTree.class.php @@ -198,6 +198,11 @@ public function imports($reflect) { return $imports; } + /** @return iterable */ + public function ofInvocable($reflect) { + throw new IllegalStateException('Invocables cannot have annotations in PHP 7'); + } + /** @return iterable */ public function ofType($reflect) { $tree= $this->tree($reflect, $reflect->getFileName()); diff --git a/src/main/php/lang/meta/MetaInformation.class.php b/src/main/php/lang/meta/MetaInformation.class.php index 57f792c..7d18a02 100755 --- a/src/main/php/lang/meta/MetaInformation.class.php +++ b/src/main/php/lang/meta/MetaInformation.class.php @@ -7,7 +7,7 @@ * it to another source otherwise. */ class MetaInformation { - private $annotations; + public $annotations; public function __construct($annotations) { $this->annotations= $annotations; diff --git a/src/main/php/lang/reflection/Invocable.class.php b/src/main/php/lang/reflection/Invocable.class.php new file mode 100755 index 0000000..e38d46e --- /dev/null +++ b/src/main/php/lang/reflection/Invocable.class.php @@ -0,0 +1,121 @@ +reflect= $arg; + } else if (is_array($arg)) { + $this->reflect= new ReflectionFunction(Closure::fromCallable($arg)); + } else { + $this->reflect= new ReflectionFunction($arg); + } + } + + /** Returns the name, null for anonymous functions */ + public function name(): ?string { + if (PHP_VERSION_ID < 80200 ? strpos($this->reflect->name, '{closure}') : $this->reflect->isAnonymous()) { + return null; + } else { + return $this->reflect->name; + } + } + + /** Returns a compound name consisting of `[NAME]()` */ + public function compoundName(): string { return $this->reflect->name.'()'; } + + /** + * Returns this functions's doc comment, or NULL if there is none. + * + * @return ?string + */ + public function comment() { + return false === ($c= $this->reflect->getDocComment()) + ? null + : trim(preg_replace('/\n\s+\* ?/', "\n", substr($c, 3, -2))) + ; + } + + /** @return lang.reflection.Annotations */ + public function annotations() { + $this->annotations ?? $this->annotations= Reflection::meta()->annotations->ofInvocable($this->reflect); + return new Annotations($this->annotations); + } + + /** @return ?lang.reflection.Annotation */ + public function annotation(string $type) { + $this->annotations ?? $this->annotations= Reflection::meta()->annotations->ofInvocable($this->reflect); + + $t= strtr($type, '.', '\\'); + return isset($this->annotations[$t]) ? new Annotation($t, $this->annotations[$t]) : null; + } + + /** + * Looks up a parameter + * + * @param int|string $arg Either a position or a name + * @return ?lang.reflection.Parameter + */ + public function parameter($arg) { + if (is_int($arg)) { + $p= $this->reflect->getParameters()[$arg] ?? null; + } else { + $p= null; + foreach ($this->reflect->getParameters() as $parameter) { + if ($arg !== $parameter->name) continue; + $p= $parameter; + break; + } + } + return null === $p ? null : new Parameter($p, $this->reflect); + } + + /** + * Returns all parameters + * + * @return lang.reflection.Parameters + */ + public function parameters(): Parameters { + return new Parameters($this->reflect); + } + + /** @return lang.reflection.Constraint */ + public function returns() { + $present= true; + $absent= function($set) use(&$present) { $present= false; }; + + $t= Type::resolve($this->reflect->getReturnType(), [], $absent); + return new Constraint($t ?? Type::$VAR, $present); + } + + /** + * Invokes this closure + * + * @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($args= []) { + try { + $pass= PHP_VERSION_ID < 80000 && $args ? Routine::pass($this->reflect, $args) : $args; + return $this->reflect->invokeArgs($pass); + } catch (ReflectionException|ArgumentCountError|TypeError $e) { + throw new CannotInvoke($this, $e); + } catch (Throwable $e) { + + // This really should be an ArgumentCountError... + if (0 === strpos($e->getMessage(), 'Unknown named parameter $')) { + throw new CannotInvoke($this, $e); + } + + throw new InvocationFailed($this, $e); + } + } +} \ 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..2dcb808 100755 --- a/src/main/php/lang/reflection/Member.class.php +++ b/src/main/php/lang/reflection/Member.class.php @@ -3,7 +3,7 @@ use lang\{XPClass, Reflection, Value}; /** Base class for constants, properties and methods */ -abstract class Member implements Annotated, Value { +abstract class Member implements Annotated, Target, Value { protected $reflect, $annotations; /** diff --git a/src/main/php/lang/reflection/Parameter.class.php b/src/main/php/lang/reflection/Parameter.class.php index cb11475..43d560f 100755 --- a/src/main/php/lang/reflection/Parameter.class.php +++ b/src/main/php/lang/reflection/Parameter.class.php @@ -15,7 +15,7 @@ class Parameter implements Annotated { * Creates a new parameter * * @param ReflectionParameter $reflect - * @param ReflectionMethod $method + * @param ReflectionFuntionAbstract $method */ public function __construct($reflect, $method= null) { $this->reflect= $reflect; diff --git a/src/main/php/lang/reflection/Parameters.class.php b/src/main/php/lang/reflection/Parameters.class.php index 6854099..66d6f8b 100755 --- a/src/main/php/lang/reflection/Parameters.class.php +++ b/src/main/php/lang/reflection/Parameters.class.php @@ -11,7 +11,7 @@ class Parameters implements \IteratorAggregate { private $method; - /** @param ReflectionMethod $method */ + /** @param ReflectionFunctionAbstract $method */ public function __construct($method) { $this->method= $method; } diff --git a/src/main/php/lang/reflection/Target.class.php b/src/main/php/lang/reflection/Target.class.php new file mode 100755 index 0000000..7303045 --- /dev/null +++ b/src/main/php/lang/reflection/Target.class.php @@ -0,0 +1,7 @@ +compoundName(), $cause); $this->target= $target; } /** Returns the target member */ - public function target(): Member { return $this->target; } + public function target(): Target { return $this->target; } } \ No newline at end of file diff --git a/src/test/php/lang/reflection/unittest/InvocableTest.class.php b/src/test/php/lang/reflection/unittest/InvocableTest.class.php new file mode 100755 index 0000000..97588eb --- /dev/null +++ b/src/test/php/lang/reflection/unittest/InvocableTest.class.php @@ -0,0 +1,121 @@ + true, null]; + yield [function() { return true; }, null]; + yield [[$this, 'name'], 'name']; + yield [[Closure::class, 'fromCallable'], 'fromCallable']; + yield [Closure::fromCallable('strtoupper'), 'strtoupper']; + yield [new ReflectionFunction('strtoupper'), 'strtoupper']; + } + + #[Test, Values(from: 'callables')] + public function can_create($callable) { + new Invocable($callable); + } + + #[Test, Values(from: 'callables')] + public function name($callable, $expected) { + Assert::equals($expected, (new Invocable($callable))->name()); + } + + #[Test] + public function no_comment() { + Assert::null((new Invocable(fn() => true))->comment()); + } + + #[Test] + public function with_comment() { + Assert::equals('Test', (new Invocable(/** Test */ fn() => true))->comment()); + } + + #[Test, Runtime(php: '>=8.0')] + public function no_annotations() { + Assert::equals([], iterator_to_array((new Invocable(fn() => true))->annotations())); + } + + #[Test, Runtime(php: '>=8.0')] + public function with_annotation() { + $closure= eval('return #[\\test\\Test] fn() => true;'); + Assert::equals('test', (new Invocable($closure))->annotation(Test::class)->name()); + } + + #[Test, Runtime(php: '>=8.0')] + public function with_annotations() { + $closure= eval('return #[\\test\\Test, \\test\\Values] fn() => true;'); + + $annotations= []; + foreach ((new Invocable($closure))->annotations() as $annotation) { + $annotations[]= $annotation->name(); + } + Assert::equals(['test', 'values'], $annotations); + } + + #[Test] + public function empty_parameters() { + $parameters= (new Invocable(fn() => true))->parameters(); + Assert::equals(0, $parameters->size()); + } + + #[Test] + public function parameters() { + $parameters= (new Invocable(fn($name) => true))->parameters(); + Assert::equals(1, $parameters->size()); + } + + #[Test, Values([0, 'name'])] + public function parameter($arg) { + $parameter= (new Invocable(fn($name) => true))->parameter($arg); + Assert::equals('name', $parameter->name()); + Assert::false($parameter->optional()); + } + + #[Test] + public function optional_parameter() { + $parameters= (new Invocable(fn($name= null) => true))->parameters(); + Assert::equals(0, $parameters->size($required= true)); + Assert::equals(1, $parameters->size($required= false)); + } + + #[Test] + public function without_return_type() { + $constraint= (new Invocable(fn($name) => true))->returns(); + Assert::false($constraint->present()); + Assert::equals(Type::$VAR, $constraint->type()); + } + + #[Test] + public function return_type() { + $constraint= (new Invocable(fn($name): bool => true))->returns(); + Assert::true($constraint->present()); + Assert::equals(Primitive::$BOOL, $constraint->type()); + } + + #[Test] + public function invoke() { + $Invocable= (new Invocable(fn($name) => strtoupper($name))); + Assert::equals('TEST', $Invocable->invoke(['test'])); + } + + #[Test] + public function invoke_named() { + $Invocable= (new Invocable(fn($name) => strtoupper($name))); + Assert::equals('TEST', $Invocable->invoke(['name' => 'test'])); + } + + #[Test, Expect(CannotInvoke::class)] + public function invoke_missing_argument() { + $Invocable= (new Invocable(fn($name) => strtoupper($name))); + $Invocable->invoke([]); + } +} \ No newline at end of file