From b6e95f477cd71d3b7f03c57e9c9028c69d64d183 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 1 Aug 2026 11:10:50 +0200 Subject: [PATCH 1/6] Add lang.ClassMeta as a replacement for lang.reflect.ClassParser --- src/main/php/lang/FunctionType.class.php | 15 ++++++++------- src/main/php/lang/XPClass.class.php | 10 ++++++++++ .../unittest/FunctionTypeVarArgsTest.class.php | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/FunctionType.class.php b/src/main/php/lang/FunctionType.class.php index 00a146352..5824778e0 100755 --- a/src/main/php/lang/FunctionType.class.php +++ b/src/main/php/lang/FunctionType.class.php @@ -76,16 +76,17 @@ public function literal(): string { * @param php.ReflectionFunctionAbstract $value * @param [lang.Type] $signature * @param function(string): var $value A function to invoke when verification fails - * @param php.ReflectionClass $class Class to get details from, optionally + * @param php.ReflectionClass $reflect Class to get details from, optionally * @return var */ - protected function verify($r, $signature, $false, $class= null) { - if ($class) { - $details= XPClass::detailsForClass(XPClass::nameOf($class->name))[1][$r->name] ?? null; + protected function verify($r, $signature, $false, $reflect= null) { + if ($reflect) { + $class= new XPClass($reflect); + $details= $class->meta()[1][$r->name] ?? null; $resolve= [ - 'static' => fn() => new XPClass($class), - 'self' => fn() => new XPClass($class), - 'parent' => fn() => new XPClass($class->getParentClass()), + 'static' => fn() => $class, + 'self' => fn() => $class, + 'parent' => fn() => new XPClass($reflect->getParentClass()), ]; } else { $details= null; diff --git a/src/main/php/lang/XPClass.class.php b/src/main/php/lang/XPClass.class.php index db2bf8631..3b1c897f5 100755 --- a/src/main/php/lang/XPClass.class.php +++ b/src/main/php/lang/XPClass.class.php @@ -249,6 +249,16 @@ protected static function _classLoaderFor($name) { } return null; // Internal class, e.g. } + + /** + * Returns `xp::$meta` for this class, extracting it if necessary + * + * @return [:var] + */ + public function meta() { + static $meta; + return \xp::$meta[$this->name]??= ($meta??= new ClassMeta())->meta($this->_class); + } /** * Retrieve details for a specified class. Note: Results from this diff --git a/src/test/php/lang/unittest/FunctionTypeVarArgsTest.class.php b/src/test/php/lang/unittest/FunctionTypeVarArgsTest.class.php index 12c1d51a5..ba2f08be6 100755 --- a/src/test/php/lang/unittest/FunctionTypeVarArgsTest.class.php +++ b/src/test/php/lang/unittest/FunctionTypeVarArgsTest.class.php @@ -18,7 +18,7 @@ private function compile($signature, $apidoc= []) { if (!isset(self::$compiled[$class])) { self::$compiled[$class]= ClassLoader::defineClass($class, null, [], '{ /** - '.implode("\n* ", $apidoc).' + * '.implode("\n* ", $apidoc).' */ public static function fixture('.$signature.') { } }'); From 6642db241581dfa2a2c4f20dc0716530b124c3f4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 1 Aug 2026 11:13:46 +0200 Subject: [PATCH 2/6] Add class meta tests --- src/main/php/lang/ClassMeta.class.php | 209 ++++++++++++++++++ .../php/lang/unittest/ClassMetaTest.class.php | 125 +++++++++++ 2 files changed, 334 insertions(+) create mode 100755 src/main/php/lang/ClassMeta.class.php create mode 100755 src/test/php/lang/unittest/ClassMetaTest.class.php diff --git a/src/main/php/lang/ClassMeta.class.php b/src/main/php/lang/ClassMeta.class.php new file mode 100755 index 000000000..aa7daa69f --- /dev/null +++ b/src/main/php/lang/ClassMeta.class.php @@ -0,0 +1,209 @@ +matching($comment, '(', ')'); + $p+= strspn($comment, ': ', $p); + return substr($comment, 0, $p).$this->type(substr($comment, $p), $reflect); + } else if (0 === strncmp($comment, '(function(', 10)) { + $p= $this->matching($comment, '(', ')'); + return substr($comment, 0, $p).$this->type(substr($comment, $p), $reflect); + } else if ('[' === $comment[0]) { + $p= $this->matching($comment, '[', ']'); + return substr($comment, 0, $p); + } else if (strstr($comment, '<')) { + $p= $this->matching($comment, '<', '>'); + $type= substr($comment, 0, $p); + } else { + $type= substr($comment, 0, strcspn($comment, ' ')); + } + + if ('\\' === ($type[0] ?? null)) { + return strtr(substr($type, 1), '\\', '.'); + } else { + return $type; + } + } + + /** + * Returns imports used in the class file the given class was declared in + * + * @param ReflectionClass $reflect + * @return [:string] + */ + public function imports($reflect) { + static $break= [T_CLASS => true, T_INTERFACE => true, T_TRAIT => true, 372 /* T_ENUM */ => true]; + static $types= [T_WHITESPACE => true, 44 => true, 59 => true, 123 => true]; + + // Exclude classes declared inside eval(), their declaration is not accessible + $file= $reflect->getFileName(); + if (false !== strpos($file, ': eval')) return []; + + $tokens= PhpToken::tokenize(file_get_contents($file)); + $imports= []; + for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) { + if (isset($break[$tokens[$i]->id])) break; + if (T_USE !== $tokens[$i]->id) continue; + + do { + $type= ''; + for ($i+= 2; $i < $s, !isset($types[$tokens[$i]->id]); $i++) { + $type.= $tokens[$i]->text; + } + + // Skip over whitespace + if (T_WHITESPACE === $tokens[$i]->id) $i++; + + // use `lang\{Type, Primitive as P}` vs. `use lang\Primitive as P;` vs. `use lang\Primitive` + if (123 === $tokens[$i]->id) { + $alias= null; + $group= ''; + for ($i+= 1; $i < $s; $i++) { + if (44 === $tokens[$i]->id) { + $imports[$alias ?? $group]= $type.$group; + $alias= null; + $group= ''; + } else if (125 === $tokens[$i]->id) { + $imports[$alias ?? $group]= $type.$group; + break; + } else if (T_AS === $tokens[$i]->id) { + $i+= 2; + $alias= $tokens[$i]->text; + } else if (T_WHITESPACE !== $tokens[$i]->id) { + $group.= $tokens[$i]->text; + } + } + } else if (T_AS === $tokens[$i]->id) { + $i+= 2; + $imports[$tokens[$i]->text]= $type; + } else if (false === ($p= strrpos($type, '\\'))) { + $imports[$type]= null; + } else { + $imports[substr($type, strrpos($type, '\\') + 1)]= $type; + } + + // Skip over whitespace + if (T_WHITESPACE === $tokens[$i]->id) $i++; + } while (44 === $tokens[$i]->id); + } + return $imports; + } + + /** + * Returns class meta information for a given class + * + * @param string|ReflectionClass|object $class + * @return [:var] + */ + public function meta($class) { + if ($class instanceof ReflectionClass) { + $reflect= $class; + } else if (is_object($class)) { + $reflect= new ReflectionClass($class); + } else { + $reflect= new ReflectionClass(strtr($class, '.', '\\')); + } + + $properties= []; + foreach ($reflect->getProperties() as $property) { + $comment= $property->getDocComment() ?: ''; + $type= null; + if (false !== ($p= strpos($comment, '* @'))) { + preg_match_all('/@([a-z]+)\s*([^\r\n]+)?/', $comment, $matches, PREG_SET_ORDER, $p + 2); + foreach ($matches as $match) { + if ('type' === $match[1]) { + $type= $this->type($match[2], $reflect); + } + } + } + + $properties[$property->name]= [ + DETAIL_RETURNS => $type, + DETAIL_COMMENT => $this->comment($comment, $p), + ]; + } + + $methods= []; + foreach ($reflect->getMethods() as $method) { + $comment= $method->getDocComment() ?: ''; + $params= $throws= []; + $returns= null; + + // Parse doc comment + if (false !== ($p= strpos($comment, '* @'))) { + preg_match_all('/@([a-z]+)\s*([^\r\n]+)?/', $comment, $matches, PREG_SET_ORDER, $p + 2); + foreach ($matches as $match) { + if ('param' === $match[1]) { + $params[]= $this->type($match[2], $reflect); + } else if ('return' === $match[1]) { + $returns= $this->type($match[2], $reflect); + } else if ('throws' === $match[1]) { + $throws[]= $this->type($match[2], $reflect); + } + } + } + + $methods[$method->name]= [ + DETAIL_ARGUMENTS => $params, + DETAIL_RETURNS => $returns, + DETAIL_THROWS => $throws, + DETAIL_COMMENT => $this->comment($comment, $p), + ]; + } + + // Returns structure compatible with xp::$meta + return [ + 'class' => [DETAIL_COMMENT => $this->comment($reflect->getDocComment() ?: '')], + $properties, + $methods, + ]; + } +} \ No newline at end of file diff --git a/src/test/php/lang/unittest/ClassMetaTest.class.php b/src/test/php/lang/unittest/ClassMetaTest.class.php new file mode 100755 index 000000000..9a7be25bb --- /dev/null +++ b/src/test/php/lang/unittest/ClassMetaTest.class.php @@ -0,0 +1,125 @@ + ''], + (new ClassMeta())->meta($class)['class'] + ); + } + + #[Test] + public function property_comment() { + $meta= (new ClassMeta())->meta(new class() { + + /** Test */ + private $test; + }); + + Assert::equals('Test', $meta[0]['test'][DETAIL_COMMENT]); + } + + #[Test] + public function property_type() { + $meta= (new ClassMeta())->meta(new class() { + + /** @type function(): int */ + private $test; + }); + + Assert::equals('function(): int', $meta[0]['test'][DETAIL_RETURNS]); + } + + #[Test] + public function method_comment() { + $meta= (new ClassMeta())->meta(new class() { + + /** + * Test + * + * @see https://example.com/ + */ + public function test() { } + }); + + Assert::equals('Test', $meta[1]['test'][DETAIL_COMMENT]); + } + + #[Test] + public function compact_doc_comment() { + $meta= (new ClassMeta())->meta(new class() { + + /** Test */ + public function test($in) { } + }); + + Assert::equals('Test', $meta[1]['test'][DETAIL_COMMENT]); + } + + #[Test] + public function param_tag() { + $meta= (new ClassMeta())->meta(new class() { + + /** @param string[] $in */ + public function test($in) { } + }); + + Assert::equals('string[]', $meta[1]['test'][DETAIL_ARGUMENTS][0]); + } + + #[Test] + public function return_tag() { + $meta= (new ClassMeta())->meta(new class() { + + /** @return string */ + public function test() { } + }); + + Assert::equals('string', $meta[1]['test'][DETAIL_RETURNS]); + } + + #[Test] + public function throws_tag() { + $meta= (new ClassMeta())->meta(new class() { + + /** @throws lang.IllegalArgumentException */ + public function test($in) { } + }); + + Assert::equals('lang.IllegalArgumentException', $meta[1]['test'][DETAIL_THROWS][0]); + } + + #[Test] + public function generic_type() { + $meta= (new ClassMeta())->meta(new class() { + + /** @return util.collection.HashTable> */ + public function test() { } + }); + + Assert::equals('util.collection.HashTable>', $meta[1]['test'][DETAIL_RETURNS]); + } + + #[Test, Ignore('Not yet supported')] + public function imported_type() { + $meta= (new ClassMeta())->meta(new class() { + + /** @return Named */ + public function test() { } + }); + + Assert::equals('lang.unittest.Named', $meta[1]['test'][DETAIL_RETURNS]); + } +} \ No newline at end of file From bfc7a1b3e0a5cd2746e71d2c9348de3274b7596f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 1 Aug 2026 11:45:38 +0200 Subject: [PATCH 3/6] Verify generic base classes using PHP 8 attributes --- src/main/php/lang/GenericTypes.class.php | 28 +++++++++---------- .../unittest/AbstractDictionary.class.php | 2 ++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index c6c97c6d4..7f47f98cb 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -26,23 +26,21 @@ public function newType(XPClass $base, array $arguments) { * @return string created type's literal */ public function newType0($base, $arguments) { - - // Verify - $details= XPClass::detailsForClass($base->getName()); - $annotations= $details ? $details['class'][DETAIL_ANNOTATIONS] : []; - if (!isset($annotations['generic']['self'])) { - throw new IllegalStateException('Class '.$base->name.' is not a generic definition'); + $generic= $base->reflect()->getAttributes(Generic::class); + if (empty($generic) || (($annotated= $generic[0]->getArguments()) && !isset($annotated['self']))) { + throw new IllegalStateException('Class '.$base->name.' is not a generic definition'); } + + // Generic(self: 'K, V') => ["K", "V"] $components= []; - foreach (Type::split($annotations['generic']['self']) as $cs => $name) { + foreach (Type::split($annotated['self']) as $name) { $components[]= ltrim($name); } - $cs++; - if ($cs !== sizeof($arguments)) { + if (sizeof($components) !== sizeof($arguments)) { throw new IllegalArgumentException(sprintf( 'Class %s expects %d component(s) <%s>, %d argument(s) given', $base->name, - $cs, + sizeof($components), implode(', ', $components), sizeof($arguments) )); @@ -58,6 +56,8 @@ public function newType0($base, $arguments) { $qname= $base->name.'<'.substr($qc, 1).'>'; // Create class if it doesn't exist yet + $details= XPClass::detailsForClass($base->getName()); + $annotations= $details ? $details['class'][DETAIL_ANNOTATIONS] : []; if (!class_exists($name, false) && !interface_exists($name, false) && !trait_exists($name, false) && !enum_exists($name, false)) { $meta= \xp::$meta[$base->name]; @@ -141,9 +141,9 @@ public function newType0($base, $arguments) { $parent.= $tokens[$i][1]; } $i--; - if (isset($annotations['generic']['parent'])) { + if (isset($annotated['parent'])) { $xargs= []; - foreach (Type::split($annotations['generic']['parent']) as $j => $placeholder) { + foreach (Type::split($annotated['parent']) as $j => $placeholder) { $xargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders)); } $src.= ' extends \\'.$this->newType0(new XPClass($base->reflect()->getParentClass()), $xargs); @@ -153,7 +153,7 @@ public function newType0($base, $arguments) { } else if (T_IMPLEMENTS === $tokens[$i][0]) { $src.= ' implements'; $counter= 0; - $annotation= $annotations['generic']['implements'] ?? null; + $annotation= $annotated['implements'] ?? null; array_unshift($state, T_CLASS); array_unshift($state, 5); } else if ('{' === $tokens[$i][0]) { @@ -167,7 +167,7 @@ public function newType0($base, $arguments) { if (T_EXTENDS === $tokens[$i][0]) { $src.= ' extends'; $counter= 0; - $annotation= $annotations['generic']['extends'] ?? null; + $annotation= $annotated['extends'] ?? null; array_unshift($state, T_INTERFACE); array_unshift($state, 5); } else if ('{' === $tokens[$i][0]) { diff --git a/src/test/php/lang/unittest/AbstractDictionary.class.php b/src/test/php/lang/unittest/AbstractDictionary.class.php index b6321ff8e..191d6b250 100755 --- a/src/test/php/lang/unittest/AbstractDictionary.class.php +++ b/src/test/php/lang/unittest/AbstractDictionary.class.php @@ -1,5 +1,7 @@ Date: Sat, 1 Aug 2026 12:25:18 +0200 Subject: [PATCH 4/6] Only fetch class details when declaring a new generic type --- src/main/php/lang/GenericTypes.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 7f47f98cb..33857dad6 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -56,9 +56,9 @@ public function newType0($base, $arguments) { $qname= $base->name.'<'.substr($qc, 1).'>'; // Create class if it doesn't exist yet - $details= XPClass::detailsForClass($base->getName()); - $annotations= $details ? $details['class'][DETAIL_ANNOTATIONS] : []; if (!class_exists($name, false) && !interface_exists($name, false) && !trait_exists($name, false) && !enum_exists($name, false)) { + $details= XPClass::detailsForClass($base->getName()); + $annotations= $details ? $details['class'][DETAIL_ANNOTATIONS] : []; $meta= \xp::$meta[$base->name]; // Parse placeholders into a lookup map From 095ed1934d2d4defe3371d74d2e1f7b83b5f2074 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 1 Aug 2026 12:38:15 +0200 Subject: [PATCH 5/6] Remove XPClass::detailsForClass() --- src/main/php/lang.base.php | 1 - src/main/php/lang/GenericTypes.class.php | 44 +++++++++++------------- src/main/php/lang/XPClass.class.php | 36 ++----------------- 3 files changed, 23 insertions(+), 58 deletions(-) diff --git a/src/main/php/lang.base.php b/src/main/php/lang.base.php index 0ce66b163..3511328c0 100755 --- a/src/main/php/lang.base.php +++ b/src/main/php/lang.base.php @@ -313,7 +313,6 @@ function newinstance($spec, $args, $def= null) { } if ($generic) { - \lang\XPClass::detailsForClass($name); xp::$meta[$name]['class'][DETAIL_GENERIC]= $generic; } diff --git a/src/main/php/lang/GenericTypes.class.php b/src/main/php/lang/GenericTypes.class.php index 33857dad6..ee3d039aa 100755 --- a/src/main/php/lang/GenericTypes.class.php +++ b/src/main/php/lang/GenericTypes.class.php @@ -26,7 +26,8 @@ public function newType(XPClass $base, array $arguments) { * @return string created type's literal */ public function newType0($base, $arguments) { - $generic= $base->reflect()->getAttributes(Generic::class); + $reflect= $base->reflect(); + $generic= $reflect->getAttributes(Generic::class); if (empty($generic) || (($annotated= $generic[0]->getArguments()) && !isset($annotated['self']))) { throw new IllegalStateException('Class '.$base->name.' is not a generic definition'); } @@ -57,9 +58,8 @@ public function newType0($base, $arguments) { // Create class if it doesn't exist yet if (!class_exists($name, false) && !interface_exists($name, false) && !trait_exists($name, false) && !enum_exists($name, false)) { - $details= XPClass::detailsForClass($base->getName()); - $annotations= $details ? $details['class'][DETAIL_ANNOTATIONS] : []; - $meta= \xp::$meta[$base->name]; + $meta= $base->meta(); + unset(\xp::$meta[$base->name]); // Parse placeholders into a lookup map $placeholders= []; @@ -184,12 +184,13 @@ public function newType0($base, $arguments) { array_unshift($state, 2); $m= $tokens[$i+ 2][1]; $p= 0; - $annotations= [$meta[1][$m][DETAIL_ANNOTATIONS] ?? [], $meta[1][$m][DETAIL_TARGET_ANNO] ?? []]; + $generic= $reflect->getMethod($m)->getAttributes(Generic::class); } else if (T_VARIABLE === $tokens[$i][0]) { $f= substr($tokens[$i][1], 1); - $annotations= $meta[0][$f][DETAIL_ANNOTATIONS] ?? []; - if (isset($annotations['generic']['var'])) { - $meta[0][$f][DETAIL_RETURNS]= strtr($annotations['generic']['var'], $placeholders); + $generic= $reflect->getProperty($f)->getAttributes(Generic::class); + $annotations= $generic ? $generic[0]->getArguments() : []; + if (isset($annotations['var'])) { + $meta[0][$f][DETAIL_RETURNS]= strtr($annotations['var'], $placeholders); } } else if ('}' === $tokens[$i][0]) { $src.= '}'; @@ -219,32 +220,31 @@ public function newType0($base, $arguments) { $default[$p].= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; } } else if (3 === $state[0]) { // Method body - if (';' === $tokens[$i][0]) { - // Abstract method - if (isset($annotations[0]['generic']['return'])) { - $meta[1][$m][DETAIL_RETURNS]= strtr($annotations[0]['generic']['return'], $placeholders); + if (';' === $tokens[$i][0]) { // Abstract method + $annotations= $generic ? $generic[0]->getArguments() : []; + if (isset($annotations['return'])) { + $meta[1][$m][DETAIL_RETURNS]= strtr($annotations['return'], $placeholders); } - if (isset($annotations[0]['generic']['params'])) { - foreach (Type::split($annotations[0]['generic']['params']) as $j => $placeholder) { + if (isset($annotations['params'])) { + foreach (Type::split($annotations['params']) as $j => $placeholder) { if ('' !== ($replaced= strtr($placeholder, $placeholders))) { $meta[1][$m][DETAIL_ARGUMENTS][$j]= $replaced; } } } - $annotations= []; - unset($meta[1][$m][DETAIL_ANNOTATIONS]['generic']); array_shift($state); } else if ('{' === $tokens[$i][0]) { $braces= 1; array_shift($state); array_unshift($state, 4); $src.= '{'; - if (isset($annotations[0]['generic']['return'])) { - $meta[1][$m][DETAIL_RETURNS]= strtr($annotations[0]['generic']['return'], $placeholders); + $annotations= $generic ? $generic[0]->getArguments() : []; + if (isset($annotations['return'])) { + $meta[1][$m][DETAIL_RETURNS]= strtr($annotations['return'], $placeholders); } - if (isset($annotations[0]['generic']['params'])) { + if (isset($annotations['params'])) { $generic= []; - foreach (Type::split($annotations[0]['generic']['params']) as $j => $placeholder) { + foreach (Type::split($annotations['params']) as $j => $placeholder) { if ('' === ($replaced= strtr($placeholder, $placeholders))) { $generic[$j]= null; } else { @@ -274,9 +274,6 @@ public function newType0($base, $arguments) { } } } - - $annotations= []; - unset($meta[1][$m][DETAIL_ANNOTATIONS]['generic']); continue; } } else if (4 === $state[0]) { // Method body @@ -337,7 +334,6 @@ public function newType0($base, $arguments) { } method_exists($name, '__static') && $name::__static(); } - unset($meta['class'][DETAIL_ANNOTATIONS]['generic']); \xp::$meta[$qname]= $meta; \xp::$cn[$name]= $qname; } diff --git a/src/main/php/lang/XPClass.class.php b/src/main/php/lang/XPClass.class.php index 3b1c897f5..af4241293 100755 --- a/src/main/php/lang/XPClass.class.php +++ b/src/main/php/lang/XPClass.class.php @@ -231,19 +231,9 @@ public function isInstance($obj): bool { } /** Retrieve the class loader a class was loaded with */ - public function getClassLoader(): IClassLoader { - return self::_classLoaderFor($this->name); - } - - /** - * Fetch a class' classloader by its name - * - * @param string name fqcn of class - * @return lang.IClassLoader - */ - protected static function _classLoaderFor($name) { - if (isset(\xp::$cl[$name])) { - sscanf(\xp::$cl[$name], '%[^:]://%[^$]', $cl, $argument); + public function getClassLoader(): ?IClassLoader { + if (isset(\xp::$cl[$this->name])) { + sscanf(\xp::$cl[$this->name], '%[^:]://%[^$]', $cl, $argument); $instanceFor= [literal($cl), 'instanceFor']; return $instanceFor($argument); } @@ -259,26 +249,6 @@ public function meta() { static $meta; return \xp::$meta[$this->name]??= ($meta??= new ClassMeta())->meta($this->_class); } - - /** - * Retrieve details for a specified class. Note: Results from this - * method are cached! - * - * @param string class fully qualified class name - * @return array or NULL to indicate no details are available - */ - public static function detailsForClass($class) { - static $parser= null; - - if (isset(\xp::$meta[$class])) return \xp::$meta[$class]; - - // Retrieve class' sourcecode - $cl= self::_classLoaderFor($class); - if (!$cl || !($bytes= $cl->loadClassBytes($class))) return null; - - $parser??= new ClassParser(); - return \xp::$meta[$class]= $parser->parseDetails($bytes); - } /** * Reflectively creates a new type From 7faae6ebee40e7997ad9c0eb13390f91b9c8fcc8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 1 Aug 2026 13:12:01 +0200 Subject: [PATCH 6/6] Remove class parser --- src/main/php/lang/XPClass.class.php | 12 +- .../php/lang/reflect/ClassParser.class.php | 699 ----------------- .../AbstractAnnotationParsingTest.class.php | 23 - .../unittest/AttributeParsingTest.class.php | 544 -------------- .../lang/unittest/ClassDetailsTest.class.php | 708 ------------------ 5 files changed, 1 insertion(+), 1985 deletions(-) delete mode 100755 src/main/php/lang/reflect/ClassParser.class.php delete mode 100755 src/test/php/lang/unittest/AbstractAnnotationParsingTest.class.php delete mode 100755 src/test/php/lang/unittest/AttributeParsingTest.class.php delete mode 100755 src/test/php/lang/unittest/ClassDetailsTest.class.php diff --git a/src/main/php/lang/XPClass.class.php b/src/main/php/lang/XPClass.class.php index af4241293..75ea1bd52 100755 --- a/src/main/php/lang/XPClass.class.php +++ b/src/main/php/lang/XPClass.class.php @@ -1,18 +1,9 @@ value($tokens, $i, $context, $imports); - } else if ('+' === $token) { - $i++; - return +1 * $this->value($tokens, $i, $context, $imports); - } else if (T_CONSTANT_ENCAPSED_STRING === $token) { - return eval('return '.$tokens[$i][1].';'); - } else if (T_LNUMBER === $tokens[$i][0]) { - if (1 === strlen($tokens[$i][1])) { - return (int)$tokens[$i][1]; - } else if ('x' === $tokens[$i][1][1]) { - return hexdec($tokens[$i][1]); - } else if ('0' === $tokens[$i][1][0]) { - return octdec($tokens[$i][1]); - } else { - return (int)$tokens[$i][1]; - } - } else if (T_DNUMBER === $tokens[$i][0]) { - return (float)$tokens[$i][1]; - } else if ('[' === $token) { - $value= []; - $element= null; - $key= 0; - for ($i++, $s= sizeof($tokens); ; $i++) { - if ($i >= $s) { - throw new IllegalStateException('Parse error: Unterminated array'); - } else if (']' === $tokens[$i]) { - $element && $value[$key]= $element[0]; - break; - } else if ('(' === $tokens[$i]) { - // Skip - } else if (',' === $tokens[$i]) { - if (!$element) throw new IllegalStateException('Parse error: Malformed array - no value before comma'); - $value[$key]= $element[0]; - $element= null; - $key= sizeof($value); - } else if (T_DOUBLE_ARROW === $tokens[$i][0]) { - $key= $element[0]; - $element= null; - } else if (T_WHITESPACE === $tokens[$i][0]) { - continue; - } else { - if ($element) throw new IllegalStateException('Parse error: Malformed array - missing comma'); - $element= [$this->value($tokens, $i, $context, $imports)]; - } - } - return $value; - } else if ('"' === $token || T_ENCAPSED_AND_WHITESPACE === $token) { - throw new IllegalStateException('Parse error: Unterminated string'); - } else if (T_FN === $token) { - $s= sizeof($tokens); - $b= 0; - $code= ''; - foreach ($imports as $name => $qualified) { - $code.= 'use '.strtr($qualified, '.', '\\').' as '.$name.';'; - } - $code.= 'return fn'; - for ($i++; $i < $s; $i++) { - if ('(' === $tokens[$i]) { - $b++; - $code.= '('; - } else if (')' === $tokens[$i]) { - $b--; - $code.= ')'; - if (0 === $b) break; - } else { - $code.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; - } - } - - // Parse expression - $b= $c= 0; - for ($i++; $i < $s; $i++) { - if ('(' === $tokens[$i]) { - $b++; - $code.= '('; - } else if (')' === $tokens[$i]) { - if (--$b < 0) break; - $code.= ')'; - } else if ('[' === $tokens[$i]) { - $c++; - $code.= '['; - } else if (']' === $tokens[$i]) { - if (--$c < 0) break; - $code.= ']'; - } else if (0 === $b && 0 === $c && ',' === $tokens[$i]) { - break; - } else { - $code.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; - } - } - $i--; - $code.= ';'; - - try { - $func= eval($code); - } catch (\ParseError $e) { - throw new IllegalStateException('In `'.$code.'`: '.$e->getMessage()); - } - if (!($func instanceof \Closure)) { - if ($error= error_get_last()) { - set_error_handler('__error', 0); - trigger_error('clear_last_error'); - restore_error_handler(); - } else { - $error= ['message' => 'Syntax error']; - } - throw new IllegalStateException('In `'.$code.'`: '.ucfirst($error['message'])); - } - return $func; - } else if (T_NEW === $token) { - $i+= 2; - $class= XPClass::forName($this->type($tokens, $i, $context, $imports)); - - $i+= 2; - for ($args= [], $arg= null, $s= sizeof($tokens); ; $i++) { - if (')' === $tokens[$i]) { - $arg && $args[]= $arg[0]; - break; - } else if (',' === $tokens[$i]) { - $args[]= $arg[0]; - $arg= null; - } else if (T_WHITESPACE !== $tokens[$i][0]) { - $arg= [$this->value($tokens, $i, $context, $imports)]; - } - } - return $class->newInstance(...$args); - } else if (T_FUNCTION === $token) { - $b= 0; - $code= ''; - foreach ($imports as $name => $qualified) { - $code.= 'use '.strtr($qualified, '.', '\\').' as '.$name.';'; - } - $code.= 'return function'; - for ($i++, $s= sizeof($tokens); $i < $s; $i++) { - if ('{' === $tokens[$i] || T_CURLY_OPEN === $tokens[$i][0]) { - $b++; - $code.= '{'; - } else if ('}' === $tokens[$i]) { - $b--; - $code.= '}'; - if (0 === $b) break; - } else { - $code.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; - } - } - try { - $func= eval($code.';'); - } catch (\ParseError $e) { - throw new IllegalStateException('In `'.$code.'`: '.$e->getMessage()); - } - if (!($func instanceof \Closure)) { - if ($error= error_get_last()) { - set_error_handler('__error', 0); - trigger_error('clear_last_error'); - restore_error_handler(); - } else { - $error= ['message' => 'Syntax error']; - } - throw new IllegalStateException('In `'.$code.'`: '.ucfirst($error['message'])); - } - return $func; - } else if (T_STRING === $token && T_NS_SEPARATOR !== $tokens[$i + 1][0] && T_DOUBLE_COLON !== $tokens[$i + 1][0]) { - if (defined($tokens[$i][1])) return constant($tokens[$i][1]); - throw new ElementNotFoundException('Undefined constant "'.$tokens[$i][1].'"'); - } else { - $class= XPClass::forName($this->type($tokens, $i, $context, $imports)); - $i+= 2; - if (T_VARIABLE === $tokens[$i][0]) { - $field= $class->reflect()->getProperty(substr($tokens[$i][1], 1)); - $m= $field->getModifiers(); - if ($m & MODIFIER_PUBLIC) { - return $field->getValue(null); - } else if (($m & MODIFIER_PROTECTED) && $class->isAssignableFrom($context['self'])) { - PHP_VERSION_ID < 80100 && $field->setAccessible(true); - return $field->getValue(null); - } else if (($m & MODIFIER_PRIVATE) && $class->getName() === $context['self']) { - PHP_VERSION_ID < 80100 && $field->setAccessible(true); - return $field->getValue(null); - } else { - throw new IllegalAccessException(sprintf( - 'Cannot access %s field %s::$%s', - implode(' ', Reflection::getModifierNames($m)), - $class->getName(), - $field->getName() - )); - } - } else if (T_CLASS === $tokens[$i][0]) { - return $class->literal(); - } else { - return $class->reflect()->getConstant($tokens[$i][1]); - } - } - } - - /** - * Parses annotation string - * - * @param string bytes - * @param [:string] context - * @return [:string] imports - * @param int line - * @return [:var] - * @throws lang.ClassFormatException - */ - public function parseAnnotations($bytes, $context, $imports= [], $line= -1) { - static $states= [ - 'annotation', 'annotation name', 'annotation value', - 'annotation named argument', 'annotation named argument value' - ]; - - // BC when class name is passed for context - if (is_string($context)) { - $parent= get_parent_class(strtr($context, '.', '\\')); - $namespace= false === ($p= strrpos($context, '.')) ? '' : substr($context, 0, $p + 1); - $context= ['self' => $context, 'parent' => $parent ? strtr($parent, '\\', '.') : null, 'namespace' => $namespace]; - } - - $tokens= token_get_all(' [], 1 => []]; - $place= $context['self'].(-1 === $line ? '' : ', line '.$line); - - // Parse tokens - try { - for ($state= 0, $i= 1, $s= sizeof($tokens); $i < $s; $i++) { - if (T_WHITESPACE === $tokens[$i][0]) { - continue; - } else if (0 === $state) { - if (']' === $tokens[$i]) { // Handle situations with trailing comma - $annotations[0][$annotation]= $value; - return $annotations; - } else { - $type= $this->type($tokens, $i, $context, $imports); - $annotation= lcfirst(false === ($p= strrpos($type, '.')) ? $type : substr($type, $p + 1)); - $annotations[1][$annotation]= $type; - $param= null; - $value= null; - $state= 1; - } - } else if (1 === $state) { // Inside attribute, check for values - if ('(' === $tokens[$i]) { - $state= 2; - } else if (',' === $tokens[$i]) { - if ($param) { - $annotations[1][$param][$annotation]= $value; - } else { - $annotations[0][$annotation]= $value; - } - $state= 0; - } else if (']' === $tokens[$i]) { - if ($param) { - $annotations[1][$param][$annotation]= $value; - } else { - $annotations[0][$annotation]= $value; - } - return $annotations; - } else if (':' === $tokens[$i]) { - $param= $annotation; - $annotation= null; - } else if (T_STRING === $tokens[$i][0]) { - $annotation= $tokens[$i][1]; - } else { - throw new IllegalStateException('Parse error: Expecting either "(", "," or "]", have '.(is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i])); - } - } else if (2 === $state) { - if (')' === $tokens[$i]) { - $state= 1; - } else if ($i + 2 < $s && (':' === $tokens[$i + 1] || ':' === $tokens[$i + 2])) { - $key= $tokens[$i][1]; - - if ('eval' === $key) { // Attribute(eval: '...') vs. Attribute(name: ...) - while ($i++ < $s && ':' === $tokens[$i] || T_WHITESPACE === $tokens[$i][0]) { } - - $code= $this->value($tokens, $i, $context, $imports); - if (is_string($code)) { - $eval= token_get_all(' $expr) { - $pairs.= "'".strtr($named, ["'" => "\\'"])."'=>{$expr},"; - } - $eval= token_get_all('value($eval, $j, $context, $imports); - } else { - $value= []; - $state= 3; - } - } else { - $value= $this->value($tokens, $i, $context, $imports); - } - } else if (3 === $state) { // Parsing key inside named arguments - if (')' === $tokens[$i]) { - $state= 1; - } else if (',' === $tokens[$i]) { - $key= null; - } else if (':' === $tokens[$i]) { - $state= 4; - } else if (is_array($tokens[$i])) { - $key= $tokens[$i][1]; - } - } else if (4 === $state) { // Parsing value inside named arguments - $value[$key]= $this->value($tokens, $i, $context, $imports); - $state= 3; - } - } - } catch (\lang\XPException $e) { - throw new ClassFormatException($e->getMessage().' in '.$place, $e); - } - throw new ClassFormatException('Parse error: Unterminated '.$states[$state].' in '.$place); - } - - /** - * Returns position of matching closing brace, or the string's length - * if no closing / opening brace is found. - * - * @param string $text - * @param string $open - * @param string $close - * @param int - */ - protected static function matching($text, $open, $close) { - for ($braces= $open.$close, $i= 0, $b= 0, $s= strlen($text); $i < $s; $i+= strcspn($text, $braces, $i)) { - if ($text[$i] === $open) { - $b++; - } else if ($text[$i] === $close) { - if (0 === --$b) return $i + 1; - } - $i++; - } - return $i; - } - - /** - * Extracts type from a text - * - * @param string $text - * @param [:string] $imports - * @return string - */ - public static function typeIn($text, $imports) { - if (0 === strncmp($text, 'function(', 9)) { - $p= self::matching($text, '(', ')'); - $p+= strspn($text, ': ', $p); - return substr($text, 0, $p).self::typeIn(substr($text, $p), $imports); - } else if (0 === strncmp($text, '(function(', 10)) { - $p= self::matching($text, '(', ')'); - return substr($text, 0, $p).self::typeIn(substr($text, $p), $imports); - } else if ('[' === $text[0]) { - $p= self::matching($text, '[', ']'); - return substr($text, 0, $p); - } else if (strstr($text, '<')) { - $p= self::matching($text, '<', '>'); - $type= substr($text, 0, $p); - } else { - $type= substr($text, 0, strcspn($text, ' ')); - } - - if ('\\' === ($type[0] ?? null)) { - return strtr(substr($type, 1), '\\', '.'); - } else if (isset($imports[$type])) { - return $imports[$type]; - } else { - return $type; - } - } - - /** - * Parse details from a given input string - * - * @param string bytes - * @return [:var] details - */ - public function parseDetails($bytes) { - $details= [[], []]; - $annotations= [0 => [], 1 => []]; - $imports= []; - $comment= ''; - $parsed= ''; - $context= ['namespace' => '', 'self' => null, 'parent' => null]; - $tokens= token_get_all($bytes); - for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) { - switch ($tokens[$i][0]) { - case T_NAMESPACE: - $namespace= ''; - for ($i+= 2; $i < $s, !(';' === $tokens[$i] || T_WHITESPACE === $tokens[$i][0]); $i++) { - $namespace.= $tokens[$i][1]; - } - $context['namespace']= strtr($namespace, '\\', '.').'.'; - break; - - case T_USE: - if (isset($details['class'])) break; // Inside class, e.g. function() use(...) {} - - do { - $type= ''; - for ($i+= 2; $i < $s, !(';' === $tokens[$i] || '{' === $tokens[$i] || ',' === $tokens[$i] || T_WHITESPACE === $tokens[$i][0]); $i++) { - $type.= $tokens[$i][1]; - } - - // use lang\{Type, Primitive as P} - if ('{' === $tokens[$i]) { - $alias= null; - $group= ''; - for ($i+= 1; $i < $s; $i++) { - if (',' === $tokens[$i]) { - $imports[$alias ? $alias : $group]= strtr($type.$group, '\\', '.'); - $alias= null; - $group= ''; - } else if ('}' === $tokens[$i]) { - $i++; - $imports[$alias ? $alias : $group]= strtr($type.$group, '\\', '.'); - break; - } else if (T_AS === $tokens[$i][0]) { - $i+= 2; - $alias= $tokens[$i][1]; - } else if (T_WHITESPACE !== $tokens[$i][0]) { - $group.= $tokens[$i][1]; - } - } - } else if (T_AS === $tokens[$i + 1][0]) { - $i+= 3; - $imports[$tokens[$i][1]]= strtr($type, '\\', '.'); - } else { - $p= strrpos($type, '\\'); - $imports[false === $p ? $type : substr($type, $p + 1)]= strtr($type, '\\', '.'); - } - } while (',' === $tokens[$i]); - break; - - case T_DOC_COMMENT: - $comment= $tokens[$i][1]; - break; - - case T_ATTRIBUTE: // PHP 8 attributes - $b= 1; - $parsed= ''; - while ($i++ < $s) { - $parsed.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; - if ('[' === $tokens[$i]) { - $b++; - } else if (']' === $tokens[$i]) { - if (0 === --$b) break; - } - } - break; - - case T_COMMENT: - if ('#' === $tokens[$i][1][0]) { // Annotations, #[@test] - if ('[' === $tokens[$i][1][1]) { - $parsed= substr($tokens[$i][1], 2); - } else { - $parsed.= substr($tokens[$i][1], 1); - } - } - break; - - case T_NEW: // Anonymous class - $details['class']= [ - DETAIL_COMMENT => null, - DETAIL_ANNOTATIONS => [], - DETAIL_ARGUMENTS => null - ]; - $annotations= [0 => [], 1 => []]; - $comment= ''; - - $b= 0; - while (++$i < $s) { - if ('(' === $tokens[$i][0]) { - $b++; - } else if (')' === $tokens[$i][0]) { - if (0 === --$b) break; - } else if (0 === $b && ';' === $tokens[$i][0]) { - break; // Abstract or interface method - } - } - break; - - case T_CLASS: - if (isset($details['class'])) break; // Inside class, e.g. $lookup= ['self' => self::class] - - case T_INTERFACE: case T_TRAIT: case T_ENUM: - $context['self']= $context['namespace'].$tokens[$i + 2][1]; - if ($parsed) { - $annotations= $this->parseAnnotations($parsed, $context, $imports, $tokens[$i][2] ?? -1); - $parsed= ''; - } - $details['class']= [ - DETAIL_COMMENT => trim(preg_replace('/\n\s+\* ?/', "\n", "\n".substr( - $comment, - 4, // "/**\n" - strpos($comment, '* @')- 2 // position of first details token - ))), - DETAIL_ANNOTATIONS => $annotations[0], - DETAIL_TARGET_ANNO => $annotations[1] - ]; - $annotations= [0 => [], 1 => []]; - $comment= ''; - break; - - case T_EXTENDS: - $i+= 2; - $context['parent']= $this->type($tokens, $i, $context, $imports); - break; - - case T_VARIABLE: // Have a member variable - if ($parsed) { - $annotations= $this->parseAnnotations($parsed, $context, $imports, $tokens[$i][2] ?? -1); - $parsed= ''; - } - $f= substr($tokens[$i][1], 1); - $details[0][$f]= [DETAIL_ANNOTATIONS => $annotations[0], DETAIL_TARGET_ANNO => $annotations[1]]; - $annotations= [0 => [], 1 => []]; - $matches= null; - if ('' === $comment) break; - preg_match_all('/@([a-z]+)\s*([^\r\n]+)?/', $comment, $matches, PREG_SET_ORDER); - foreach ((array)$matches as $match) { - if ('var' === $match[1] || 'type' === $match[1]) { - $details[0][$f][DETAIL_RETURNS]= self::typeIn($match[2], $imports); - } - } - $comment= ''; - break; - - case T_FUNCTION: - if ($parsed) { - $annotations= $this->parseAnnotations($parsed, $context, $imports, $tokens[$i][2] ?? -1); - $parsed= ''; - } - $i+= 2; - $m= $tokens[$i][1]; - $details[1][$m]= [ - DETAIL_ARGUMENTS => [], - DETAIL_RETURNS => null, - DETAIL_THROWS => [], - DETAIL_COMMENT => trim(preg_replace('/\n\s+\* ?/', "\n", "\n".substr( - $comment, - 4, // "/**\n" - strpos($comment, '* @')- 2 // position of first details token - ))), - DETAIL_ANNOTATIONS => $annotations[0], - DETAIL_TARGET_ANNO => $annotations[1] - ]; - $annotations= [0 => [], 1 => []]; - $matches= null; - preg_match_all('/@([a-z]+)\s*([^\r\n]+)?/', $comment, $matches, PREG_SET_ORDER); - $comment= ''; - $arg= 0; - foreach ($matches as $match) { - switch ($match[1]) { - case 'param': - $details[1][$m][DETAIL_ARGUMENTS][$arg++]= self::typeIn($match[2], $imports); - break; - - case 'return': - $details[1][$m][DETAIL_RETURNS]= self::typeIn($match[2], $imports); - break; - - case 'throws': - $details[1][$m][DETAIL_THROWS][]= self::typeIn($match[2], $imports); - break; - } - } - - $b= 0; - $parsed= null; - while (++$i < $s) { - if ('(' === $tokens[$i][0]) { - $b++; - } else if (')' === $tokens[$i][0]) { - if (0 === --$b) break; - } else if (T_COMMENT === $tokens[$i][0]) { - $parsed= $tokens[$i][1]; - } else if (T_ATTRIBUTE === $tokens[$i][0]) { - $e= 1; - $parsed= ''; - while ($i++ < $s) { - $parsed.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; - if ('[' === $tokens[$i]) { - $e++; - } else if (']' === $tokens[$i]) { - if (0 === --$e) break; - } - } - } else if (T_VARIABLE === $tokens[$i][0] && null !== $parsed) { - $annotations= $this->parseAnnotations( - $parsed, - $context, - $imports, - $tokens[$i][2] ?? -1 - ); - $details[1][$m][DETAIL_TARGET_ANNO][$tokens[$i][1]]= $annotations[0]; - $details[1][$m][DETAIL_TARGET_ANNO]+= $annotations[1]; - $parsed= null; - } - } - - $b= 0; - while (++$i < $s) { - if ('{' === $tokens[$i] || T_CURLY_OPEN === $tokens[$i][0]) { - $b++; - } else if ('}' === $tokens[$i]) { - if (0 === --$b) break; - } else if (0 === $b && ';' === $tokens[$i]) { - break; // Abstract or interface method - } - } - break; - - default: - // Empty - } - } - return $details; - } -} \ No newline at end of file diff --git a/src/test/php/lang/unittest/AbstractAnnotationParsingTest.class.php b/src/test/php/lang/unittest/AbstractAnnotationParsingTest.class.php deleted file mode 100755 index 9e3bba511..000000000 --- a/src/test/php/lang/unittest/AbstractAnnotationParsingTest.class.php +++ /dev/null @@ -1,23 +0,0 @@ -parseAnnotations($input, nameof($this), [ - 'Namespaced' => 'lang.unittest.fixture.Namespaced' - ]); - } -} \ No newline at end of file diff --git a/src/test/php/lang/unittest/AttributeParsingTest.class.php b/src/test/php/lang/unittest/AttributeParsingTest.class.php deleted file mode 100755 index 49e3b83a9..000000000 --- a/src/test/php/lang/unittest/AttributeParsingTest.class.php +++ /dev/null @@ -1,544 +0,0 @@ -parseAnnotations($input, nameof($this), array_merge($imports, [ - 'Namespaced' => 'lang.unittest.fixture.Namespaced' - ])); - } - - #[Test, Values(['#[Hello]', '#[Hello()]'])] - public function no_value($declaration) { - Assert::equals( - [0 => ['hello' => null], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse($declaration) - ); - } - - - #[Test] - public function sq_string_value() { - Assert::equals( - [0 => ['hello' => 'World'], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse("#[Hello('World')]") - ); - } - - #[Test] - public function sq_string_value_with_equals_sign() { - Assert::equals( - [0 => ['hello' => 'World=Welt'], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse("#[Hello('World=Welt')]") - ); - } - - #[Test] - public function sq_string_value_with_at_sign() { - Assert::equals( - [0 => ['hello' => '@World'], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse("#[Hello('@World')]") - ); - } - - #[Test] - public function sq_string_value_with_annotation() { - Assert::equals( - [0 => ['hello' => '@hello("World")'], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse("#[Hello('@hello(\"World\")')]") - ); - } - - #[Test] - public function sq_string_value_with_double_quotes() { - Assert::equals( - [0 => ['hello' => 'said "he"'], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse("#[Hello('said \"he\"')]") - ); - } - - #[Test] - public function sq_string_value_with_escaped_single_quotes() { - Assert::equals( - [0 => ['hello' => "said 'he'"], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse("#[Hello('said \'he\'')]") - ); - } - - #[Test] - public function dq_string_value() { - Assert::equals( - [0 => ['hello' => 'World'], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse('#[Hello("World")]') - ); - } - - #[Test] - public function dq_string_value_with_single_quote() { - Assert::equals( - [0 => ['hello' => 'Beck\'s'], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse('#[Hello("Beck\'s")]') - ); - } - - #[Test] - public function dq_string_value_with_escaped_double_quotes() { - Assert::equals( - [0 => ['hello' => 'said "he"'], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse('#[Hello("said \"he\"")]') - ); - } - - #[Test] - public function dq_string_value_with_escape_sequence() { - Assert::equals( - [0 => ['hello' => "World\n"], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse('#[Hello("World\n")]') - ); - } - - #[Test] - public function dq_string_value_with_at_sign() { - Assert::equals( - [0 => ['hello' => '@World'], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse('#[Hello("@World")]') - ); - } - - #[Test] - public function dq_string_value_with_annotation() { - Assert::equals( - [0 => ['hello' => '@hello(\'World\')'], 1 => ['hello' => 'lang.unittest.Hello']], - $this->parse('#[Hello("@hello(\'World\')")]') - ); - } - - #[Test] - public function int_value() { - Assert::equals( - [0 => ['answer' => 42], 1 => ['answer' => 'lang.unittest.Answer']], - $this->parse('#[Answer(42)]') - ); - } - - #[Test] - public function double_value() { - Assert::equals( - [0 => ['version' => 3.5], 1 => ['version' => 'lang.unittest.Version']], - $this->parse('#[Version(3.5)]') - ); - } - - #[Test] - public function multi_value_using_array() { - Assert::equals( - [0 => ['xmlMapping' => ['hw_server', 'server']], 1 => ['xmlMapping' => 'lang.unittest.XmlMapping']], - $this->parse("#[XmlMapping(['hw_server', 'server'])]") - ); - } - - #[Test] - public function array_value() { - Assert::equals( - [0 => ['versions' => [3.4, 3.5]], 1 => ['versions' => 'lang.unittest.Versions']], - $this->parse('#[Versions([3.4, 3.5])]') - ); - } - - #[Test] - public function array_value_with_nested_arrays() { - Assert::equals( - [0 => ['versions' => [[3], [4]]], 1 => ['versions' => 'lang.unittest.Versions']], - $this->parse('#[Versions([[3], [4]])]') - ); - } - - #[Test] - public function array_value_with_strings_containing_braces() { - Assert::equals( - [0 => ['versions' => ['(3..4]']], 1 => ['versions' => 'lang.unittest.Versions']], - $this->parse('#[Versions(["(3..4]"])]') - ); - } - - #[Test] - public function bool_true_value() { - Assert::equals( - [0 => ['supported' => true], 1 => ['supported' => 'lang.unittest.Supported']], - $this->parse('#[Supported(true)]') - ); - } - - #[Test] - public function bool_false_value() { - Assert::equals( - [0 => ['supported' => false], 1 => ['supported' => 'lang.unittest.Supported']], - $this->parse('#[Supported(false)]') - ); - } - - #[Test] - public function named_arguments() { - Assert::equals( - [ - 0 => ['config' => ['key' => 'value', 'times' => 5, 'disabled' => false, 'null' => null, 'list' => [1, 2]]], - 1 => ['config' => 'lang.unittest.Config'] - ], - $this->parse("#[Config(key: 'value', times: 5, disabled: false, null: null, list: [1, 2])]") - ); - \xp::gc(); - } - - #[Test] - public function map_value() { - Assert::equals( - [0 => ['colors' => ['green' => '$10.50', 'red' => '$9.99']], 1 => ['colors' => 'lang.unittest.Colors']], - $this->parse("#[Colors(['green' => '$10.50', 'red' => '$9.99'])]") - ); - } - - #[Test] - public function multi_line_annotation() { - Assert::equals( - [ - 0 => ['interceptors' => ['classes' => [ - 'lang.unittest.FirstInterceptor', - 'lang.unittest.SecondInterceptor', - ]]], - 1 => ['interceptors' => 'lang.unittest.Interceptors'] - ], - $this->parse(" - #[Interceptors(classes: [ - 'lang.unittest.FirstInterceptor', - 'lang.unittest.SecondInterceptor', - ])] - ") - ); - } - - #[Test] - public function simple_XPath_annotation() { - Assert::equals( - [ - 0 => ['fromXml' => ['xpath' => '/parent/child/@attribute']], - 1 => ['fromXml' => 'lang.unittest.FromXml'] - ], - $this->parse("#[FromXml(['xpath' => '/parent/child/@attribute'])]") - ); - } - - #[Test] - public function complex_XPath_annotation() { - Assert::equals( - [ - 0 => ['fromXml' => ['xpath' => '/parent[@attr="value"]/child[@attr1="val1" and @attr2="val2"]']], - 1 => ['fromXml' => 'lang.unittest.FromXml'] - ], - $this->parse("#[FromXml(['xpath' => '/parent[@attr=\"value\"]/child[@attr1=\"val1\" and @attr2=\"val2\"]'])]") - ); - } - - #[Test] - public function string_with_equal_signs() { - Assert::equals( - [0 => ['permission' => 'rn=login, rt=config'], 1 => ['permission' => 'lang.unittest.Permission']], - $this->parse("#[Permission('rn=login, rt=config')]") - ); - } - - #[Test] - public function string_assigned_without_whitespace() { - Assert::equals( - [0 => ['arg' => ['name' => 'verbose', 'short' => 'v']], 1 => ['arg' => 'lang.unittest.Arg']], - $this->parse("#[Arg(['name' => 'verbose', 'short' => 'v'])]") - ); - } - - #[Test] - public function multiple_values_with_strings_and_equal_signs() { - Assert::equals( - [ - 0 => ['permission' => ['names' => ['rn=login, rt=config1', 'rn=login, rt=config2']]], - 1 => ['permission' => 'lang.unittest.Permission'] - ], - $this->parse("#[Permission(['names' => ['rn=login, rt=config1', 'rn=login, rt=config2']])]") - ); - } - - #[Test] - public function unittest_annotation() { - Assert::equals( - [ - 0 => ['test' => NULL, 'ignore' => NULL, 'limit' => ['time' => 0.1, 'memory' => 100]], - 1 => ['test' => 'lang.unittest.Test', 'ignore' => 'lang.unittest.Ignore', 'limit' => 'lang.unittest.Limit'] - ], - $this->parse("#[Test, Ignore, Limit(['time' => 0.1, 'memory' => 100])]") - ); - } - - #[Test] - public function overloaded_annotation() { - Assert::equals( - [ - 0 => ['overloaded' => ['signatures' => [['string'], ['string', 'string']]]], - 1 => ['overloaded' => 'lang.unittest.Overloaded'] - ], - $this->parse('#[Overloaded(["signatures" => [["string"], ["string", "string"]]])]') - ); - } - - #[Test] - public function overloaded_annotation_spanning_multiple_lines() { - Assert::equals( - [ - 0 => ['overloaded' => ['signatures' => [['string'], ['string', 'string']]]], - 1 => ['overloaded' => 'lang.unittest.Overloaded'] - ], - $this->parse( - "#[Overloaded(['signatures' => [\n". - " ['string'],\n". - " ['string', 'string']\n". - "]])]" - ) - ); - } - - #[Test] - public function map_value_with_short_syntax() { - Assert::equals( - [ - 0 => ['colors' => ['green' => '$10.50', 'red' => '$9.99']], - 1 => ['colors' => 'lang.unittest.Colors'] - ], - $this->parse("#[Colors(['green' => '$10.50', 'red' => '$9.99'])]") - ); - } - - #[Test] - public function array_syntax_as_value() { - Assert::equals( - [ - 0 => ['permissions' => ['rn=login, rt=config', 'rn=admin, rt=config']], - 1 => ['permissions' => 'lang.unittest.Permissions'] - ], - $this->parse("#[Permissions(['rn=login, rt=config', 'rn=admin, rt=config'])]") - ); - } - - #[Test] - public function array_syntax_as_key() { - Assert::equals( - [ - 0 => ['permissions' => ['names' => ['rn=login, rt=config', 'rn=admin, rt=config']]], - 1 => ['permissions' => 'lang.unittest.Permissions'] - ], - $this->parse("#[Permissions(['names' => ['rn=login, rt=config', 'rn=admin, rt=config']])]") - ); - } - - #[Test] - public function nested_array_syntax() { - Assert::equals( - [0 => ['values' => [[1, 1], [2, 2], [3, 3]]], 1 => ['values' => 'lang.unittest.Values']], - $this->parse("#[Values([[1, 1], [2, 2], [3, 3]])]") - ); - } - - #[Test] - public function nested_array_syntax_as_key() { - Assert::equals( - [0 => ['test' => ['values' => [[1, 1], [2, 2], [3, 3]]]], 1 => ['test' => 'lang.unittest.Test']], - $this->parse("#[Test(['values' => [[1, 1], [2, 2], [3, 3]]])]") - ); - } - - #[Test] - public function negative_and_positive_floats_inside_array() { - Assert::equals( - [0 => ['values' => [0.0, -1.5, +1.5]], 1 => ['values' => 'lang.unittest.Values']], - $this->parse("#[Values([0.0, -1.5, +1.5])]") - ); - } - - #[Test] - public function class_instance_value() { - Assert::equals( - [0 => ['value' => new Name('hello')], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(new Name("hello"))]') - ); - } - - #[Test] - public function imported_class_instance_value() { - Assert::equals( - [0 => ['value' => new Name('hello')], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(new Name("hello"))]', ['Name' => 'lang.unittest.Name']) - ); - } - - #[Test] - public function fully_qualified_class_instance_value() { - Assert::equals( - [0 => ['value' => new Name('hello')], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(new \lang\unittest\Name("hello"))]') - ); - } - - #[Test] - public function fully_qualified_not_loaded_class() { - $this->parse('#[Value(new \lang\unittest\NotLoaded())]'); - } - - - #[Test] - public function class_constant_via_self() { - Assert::equals( - [0 => ['value' => 'constant'], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(self::CONSTANT)]') - ); - } - - #[Test] - public function class_constant_via_parent() { - Assert::equals( - [0 => ['value' => 'constant'], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(parent::PARENTS_CONSTANT)]') - ); - } - - #[Test] - public function class_constant_via_classname() { - Assert::equals( - [0 => ['value' => 'constant'], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(AttributeParsingTest::CONSTANT)]') - ); - } - - #[Test] - public function class_constant_via_ns_classname() { - Assert::equals( - [0 => ['value' => 'constant'], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(\lang\unittest\AttributeParsingTest::CONSTANT)]') - ); - } - - #[Test] - public function class_constant_via_imported_classname() { - Assert::equals( - [0 => ['value' => 'namespaced'], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(Namespaced::CONSTANT)]') - ); - } - - #[Test] - public function class_constant_via_self_in_map() { - Assert::equals( - [0 => ['map' => ['key' => 'constant', 'value' => 'val']], 1 => ['map' => 'lang.unittest.Map']], - $this->parse('#[Map(["key" => self::CONSTANT, "value" => "val"])]') - ); - } - - #[Test] - public function class_constant_via_classname_in_map() { - Assert::equals( - [0 => ['map' => ['key' => 'constant', 'value' => 'val']], 1 => ['map' => 'lang.unittest.Map']], - $this->parse('#[Map(["key" => AttributeParsingTest::CONSTANT, "value" => "val"])]') - ); - } - - #[Test] - public function class_constant_via_ns_classname_in_map() { - Assert::equals( - [0 => ['map' => ['key' => 'constant', 'value' => 'val']], 1 => ['map' => 'lang.unittest.Map']], - $this->parse('#[Map(["key" => \lang\unittest\AttributeParsingTest::CONSTANT, "value" => "val"])]') - ); - } - - #[Test] - public function class_public_static_member() { - Assert::equals( - [0 => ['value' => 'exposed'], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(eval: "self::\$exposed")]') - ); - } - - #[Test] - public function parent_public_static_member() { - Assert::equals( - [0 => ['value' => 'exposed'], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(eval: "parent::\$parentsExposed")]') - ); - } - - #[Test] - public function class_protected_static_member() { - Assert::equals( - [0 => ['value' => 'hidden'], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(eval: "self::\$hidden")]') - ); - } - - #[Test] - public function parent_protected_static_member() { - Assert::equals( - [0 => ['value' => 'hidden'], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(eval: "parent::\$parentsHidden")]') - ); - } - - #[Test] - public function class_private_static_member() { - Assert::equals( - [0 => ['value' => 'internal'], 1 => ['value' => 'lang.unittest.Value']], - $this->parse('#[Value(eval: "self::\$internal")]') - ); - } - - #[Test, Expect(class: ClassFormatException::class, message: '/Cannot access private static field .+AbstractAnnotationParsingTest::\$parentsInternal/')] - public function parent_private_static_member() { - $this->parse('#[Value(eval: "parent::\$parentsInternal")]'); - } - - #[Test] - public function closurel() { - $annotation= $this->parse('#[Value(eval: "function() { return true; }")]'); - Assert::instance('Closure', $annotation[0]['value']); - } - - #[Test] - public function closuresl() { - $annotation= $this->parse('#[Values(eval: "[ - function() { return true; }, - function() { return false; } - ]")]'); - Assert::instance('Closure[]', $annotation[0]['values']); - } - - #[Test] - public function short_closure_via_eval() { - $annotation= $this->parse('#[Value(eval: "fn() => true")]'); - Assert::instance('Closure', $annotation[0]['value']); - } - - #[Test, Values(['#[Value(eval: "fn() => new Name(\"Test\")")]', '#[Value(eval: "function() { return new Name(\"Test\"); }")]',])] - public function imports_in_closures($closure) { - $annotation= $this->parse($closure, ['Name' => 'lang.unittest.Name']); - Assert::instance(Name::class, $annotation[0]['value']()); - } -} \ No newline at end of file diff --git a/src/test/php/lang/unittest/ClassDetailsTest.class.php b/src/test/php/lang/unittest/ClassDetailsTest.class.php deleted file mode 100755 index 7e52acd91..000000000 --- a/src/test/php/lang/unittest/ClassDetailsTest.class.php +++ /dev/null @@ -1,708 +0,0 @@ -parseDetails(' - ', - self::class - ); - return $details[1]['test']; - } - - #[Test, Values(['class', 'interface', 'trait'])] - public function parses($kind) { - $details= (new ClassParser())->parseDetails(' '', DETAIL_ANNOTATIONS => [], DETAIL_TARGET_ANNO => []], - $details['class'] - ); - } - - #[Test] - public function commentString() { - $details= $this->parseComment(' - /** - * A protected method - * - * Note: Not compatible with PHP 4.1.2! - * - * @param string param1 - */ - '); - Assert::equals( - "A protected method\n\nNote: Not compatible with PHP 4.1.2!", - $details[DETAIL_COMMENT] - ); - } - - #[Test] - public function noCommentString() { - $details= $this->parseComment(' - /** - * @see php://comment - */ - '); - Assert::equals('', $details[DETAIL_COMMENT]); - } - - #[Test] - public function scalar_parameter() { - $details= $this->parseComment('/** @param string param1 */'); - Assert::equals('string', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function array_parameter() { - $details= $this->parseComment('/** @param string[] param1 */'); - Assert::equals('string[]', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function object_parameter() { - $details= $this->parseComment('/** @param util.Date param1 */'); - Assert::equals('util.Date', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function defaultParameter() { - $details= $this->parseComment('/** @param int param1 default 1 */'); - Assert::equals('int', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function map_parameter() { - $details= $this->parseComment('/** @param [:string] map */'); - Assert::equals('[:string]', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function generic_parameter_with_two_components() { - $details= $this->parseComment('/** @param util.collection.HashTable map */'); - Assert::equals('util.collection.HashTable', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function generic_parameter_with_one_component() { - $details= $this->parseComment('/** @param util.collections.Vector param1 */'); - Assert::equals('util.collections.Vector', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function nested_generic_parameter() { - $details= $this->parseComment('/** @param util.collections.Vector> map */'); - Assert::equals('util.collections.Vector>', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function function_parameter() { - $details= $this->parseComment('/** @param function(string): string param1 */'); - Assert::equals('function(string): string', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function function_accepting_function_parameter() { - $details= $this->parseComment('/** @param function(function(): void): string param1 */'); - Assert::equals('function(function(): void): string', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function function_returning_function_parameter() { - $details= $this->parseComment('/** @param function(): function(): void param1 */'); - Assert::equals('function(): function(): void', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function function_returning_generic() { - $details= $this->parseComment('/** @param function(): util.Filter param1 */'); - Assert::equals('function(): util.Filter', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function function_returning_array() { - $details= $this->parseComment('/** @param function(): int[] param1 */'); - Assert::equals('function(): int[]', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function braced_function_type() { - $details= $this->parseComment('/** @param (function(): int) param1 */'); - Assert::equals('(function(): int)', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function array_of_functions() { - $details= $this->parseComment('/** @param (function(): int)[] param1 */'); - Assert::equals('(function(): int)[]', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function map_of_functions() { - $details= $this->parseComment('/** @param [:function(): int] param1 */'); - Assert::equals('[:function(): int]', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function map_of_functions_with_braces() { - $details= $this->parseComment('/** @param [:(function(): int)] param1 */'); - Assert::equals('[:(function(): int)]', $details[DETAIL_ARGUMENTS][0]); - } - - #[Test] - public function throwsList() { - $details= $this->parseComment(' - /** - * Test method - * - * @throws lang.IllegalArgumentException - * @throws lang.IllegalAccessException - */ - '); - Assert::equals('lang.IllegalArgumentException', $details[DETAIL_THROWS][0]); - Assert::equals('lang.IllegalAccessException', $details[DETAIL_THROWS][1]); - } - - #[Test] - public function int_return_type() { - $details= $this->parseComment('/** @return int */'); - Assert::equals('int', $details[DETAIL_RETURNS]); - } - - #[Test] - public function withClosure() { - $details= (new ClassParser())->parseDetails(''); - Assert::equals('Creates a new answer', $details[1]['newAnswer'][DETAIL_COMMENT]); - } - - #[Test] - public function withClosures() { - $details= (new ClassParser())->parseDetails(''); - Assert::equals('Creates a new question', $details[1]['newQuestion'][DETAIL_COMMENT]); - } - - #[Test] - public function use_statements_evaluated() { - $actual= (new ClassParser())->parseDetails('parseDetails('parseDetails('parseDetails('parseDetails('parseDetails('parseDetails(' 'test'], $actual['class'][DETAIL_ANNOTATIONS]); - } - - #[Test] - public function php8_attributes_with_trailing_comma() { - $actual= (new ClassParser())->parseDetails(' null, 'value' => 'test'], $actual['class'][DETAIL_ANNOTATIONS]); - } - - #[Test, Values(['\lang\unittest\Name', 'unittest\Name', 'Name'])] - public function php8_attributes_with_named_arguments($name) { - $actual= (new ClassParser())->parseDetails(' ['class' => Name::class]], $actual['class'][DETAIL_ANNOTATIONS]); - } - - #[Test] - public function php8_attributes_with_eval_argument() { - $actual= (new ClassParser())->parseDetails('parseDetails('parseDetails(' "function() { return \"test\"; }"])] - class Test { - } - '); - Assert::equals('test', $actual['class'][DETAIL_ANNOTATIONS]['value']['func']()); - } - - #[Test] - public function php8_attributes_with_multiple_named_array_eval_arguments() { - $actual= (new ClassParser())->parseDetails(' "1", "two" => "2"])] - class Test { - } - '); - Assert::equals(['one' => 1, 'two' => 2], $actual['class'][DETAIL_ANNOTATIONS]['value']); - } - - #[Test] - public function absolute_compound_php8_attributes() { - $actual= (new ClassParser())->parseDetails(' null], $actual['class'][DETAIL_ANNOTATIONS]); - } - - #[Test] - public function relative_compound_php8_attributes() { - $actual= (new ClassParser())->parseDetails(' null], $actual['class'][DETAIL_ANNOTATIONS]); - } - - #[Test, Expect(class: ClassFormatException::class, message: '/Unexpected ","/')] - public function php8_attributes_cannot_have_multiple_arguments() { - (new ClassParser())->parseDetails('parseDetails(' 'Value'], $actual['class'][DETAIL_TARGET_ANNO]); - } - - #[Test] - public function php8_method_attributes_original_names() { - $actual= (new ClassParser())->parseDetails(' 'Value'], $actual[1]['fixture'][DETAIL_TARGET_ANNO]); - } - - #[Test] - public function php8_field_attributes_original_names() { - $actual= (new ClassParser())->parseDetails(' 'Value'], $actual[0]['fixture'][DETAIL_TARGET_ANNO]); - } - - #[Test] - public function php8_param_attributes_original_names() { - $actual= (new ClassParser())->parseDetails(' ['value' => 'test'], 'value' => 'Value'], $actual[1]['fixture'][DETAIL_TARGET_ANNO]); - } - - #[Test, Expect(class: ClassFormatException::class, message: '/Unexpected "," in eval/')] - public function array_eval_cannot_have_multiple_arguments() { - (new ClassParser())->parseDetails('parseDetails('parseDetails('parseDetails(' null], $details[0]['fixture'][DETAIL_ANNOTATIONS]); - } - - #[Test] - public function method_annotations() { - $details= (new ClassParser())->parseDetails(' null], $details[1]['fixture'][DETAIL_ANNOTATIONS]); - } - - #[Test] - public function abstract_method_annotations() { - $details= (new ClassParser())->parseDetails(' null], $details[1]['fixture'][DETAIL_ANNOTATIONS]); - } - - #[Test] - public function interface_method_annotations() { - $details= (new ClassParser())->parseDetails(' null], $details[1]['fixture'][DETAIL_ANNOTATIONS]); - } - - #[Test] - public function parser_not_confused_by_closure() { - $details= (new ClassParser())->parseDetails(' null], $details[1]['fixture'][DETAIL_ANNOTATIONS]); - } - - #[Test] - public function field_with_annotation_after_methods() { - $details= (new ClassParser())->parseDetails(' null], $details[0]['fixture'][DETAIL_ANNOTATIONS]); - } - - #[Test, Values(['\lang\unittest\Name', 'unittest\Name', 'Name'])] - public function annotation_with_reference_to($parent) { - $details= (new ClassParser())->parseDetails(' new Name('Test')], $details[1]['fixture'][DETAIL_ANNOTATIONS]); - } - - #[Test, Expect(class: ClassFormatException::class, message: '/Class does not have a parent/')] - public function annotation_with_parent_reference_in_parentless_class() { - (new ClassParser())->parseDetails('parseDetails(' '', DETAIL_ANNOTATIONS => [], DETAIL_TARGET_ANNO => []], - $details['class'] - ); - } - - #[Test, Values([['null', null], ['false', false], ['true', true], ['0', 0], ['1', 1], ['-1', -1], ['0x0', 0], ['0x2a', 42], ['00', 0], ['0644', 420], ['0.0', 0.0], ['1.5', 1.5], ['-1.5', -1.5], ['"hello"', 'hello'], ['""', ''], ['[1, 2, 3]', [1, 2, 3]], ['[]', []], ['["key" => "value"]', ['key' => 'value']],])] - public function annotation_values($literal, $value) { - $details= (new ClassParser())->parseDetails(' $value], $details[1]['fixture'][DETAIL_ANNOTATIONS]); - } - - #[Test, Values(['function() { return "Test"; }', 'fn() => "Test"',])] - public function closures_inside_annotations($declaration) { - $details= (new ClassParser())->parseDetails('parseDetails(' ord(chr(42)))] - public abstract function fixture(); - } - '); - Assert::equals(42, $details[1]['fixture'][DETAIL_ANNOTATIONS]['call']()); - } - - #[Test, Values(['[fn() => [1, 2, 3]]', '[fn() => [1, 2, 3], ]', '[fn() => [1, 2, 3], 1]', '[fn() => [[1, [2][0], 3]][0]]',])] - public function fn_with_arrays($declaration) { - $details= (new ClassParser())->parseDetails('parseDetails(' null, DETAIL_ANNOTATIONS => [], DETAIL_ARGUMENTS => null], - $details['class'] - ); - } - - #[Test] - public function anonymous_class_with_arguments() { - $details= (new ClassParser())->parseDetails(' null, DETAIL_ANNOTATIONS => [], DETAIL_ARGUMENTS => null], - $details['class'] - ); - } - - #[Test] - public function anonymous_class_member() { - $details= (new ClassParser())->parseDetails(' null], $details[0]['fixture'][DETAIL_ANNOTATIONS]); - } - - #[Test] - public function anonymous_class_method() { - $details= (new ClassParser())->parseDetails(' null], $details[1]['fixture'][DETAIL_ANNOTATIONS]); - } - - #[Test] - public function new_after_curly_open_in_string() { - $details= (new ClassParser())->parseDetails('name}.txt"; - new File($name); - } - } - '); - Assert::equals( - [DETAIL_COMMENT => 'Comment', DETAIL_ANNOTATIONS => [], DETAIL_TARGET_ANNO => []], - $details['class'] - ); - } - - #[Test] - public function annotation_with_curly_open_in_string() { - $details= (new ClassParser())->parseDetails('