diff --git a/src/main/php/lang/meta/FromAttributes.class.php b/src/main/php/lang/meta/FromAttributes.class.php index 2f58229..affd739 100755 --- a/src/main/php/lang/meta/FromAttributes.class.php +++ b/src/main/php/lang/meta/FromAttributes.class.php @@ -66,14 +66,24 @@ public function imports($reflect) { static $types= [T_WHITESPACE => true, 44 => true, 59 => true, 123 => true]; $tokens= PhpToken::tokenize(file_get_contents($reflect->getFileName())); - $imports= []; + $imports= ['class' => [], 'function' => [], 'const' => []]; 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++) { + $i+= 2; + if (T_FUNCTION === $tokens[$i]->id) { + $kind= 'function'; + $i+= 2; + } else if (T_CONST === $tokens[$i]->id) { + $kind= 'const'; + $i+= 2; + } else { + $kind= 'class'; + } + + for ($type= ''; $i < $s, !isset($types[$tokens[$i]->id]); $i++) { $type.= $tokens[$i]->text; } @@ -86,11 +96,11 @@ public function imports($reflect) { $group= ''; for ($i+= 1; $i < $s; $i++) { if (44 === $tokens[$i]->id) { - $imports[$alias ?? $group]= $type.$group; + $imports[$kind][$alias ?? $group]= $type.$group; $alias= null; $group= ''; } else if (125 === $tokens[$i]->id) { - $imports[$alias ?? $group]= $type.$group; + $imports[$kind][$alias ?? $group]= $type.$group; break; } else if (T_AS === $tokens[$i]->id) { $i+= 2; @@ -101,11 +111,11 @@ public function imports($reflect) { } } else if (T_AS === $tokens[$i]->id) { $i+= 2; - $imports[$tokens[$i]->text]= $type; + $imports[$kind][$tokens[$i]->text]= $type; } else if (false === ($p= strrpos($type, '\\'))) { - $imports[$type]= null; + $imports[$kind][$type]= $type; } else { - $imports[substr($type, strrpos($type, '\\') + 1)]= $type; + $imports[$kind][substr($type, strrpos($type, '\\') + 1)]= $type; } // Skip over whitespace @@ -120,8 +130,13 @@ public function evaluate($reflect, $code) { if ($namespace= $reflect->getNamespaceName()) { $header.= 'namespace '.$namespace.';'; } - foreach ($this->imports($reflect) as $import => $type) { - $header.= $type ? "use {$type} as {$import};" : "use {$import};"; + + // Recreate all imports + foreach ($this->imports($reflect) as $kind => $list) { + $use= 'class' === $kind ? 'use' : 'use '.$kind; + foreach ($list as $import => $type) { + $header.= $type ? "{$use} {$type} as {$import};" : "{$use} {$import};"; + } } $f= eval($header.' return static function() { return '.$code.'; };'); diff --git a/src/main/php/lang/meta/FromSyntaxTree.class.php b/src/main/php/lang/meta/FromSyntaxTree.class.php index 38f1e79..54f996e 100755 --- a/src/main/php/lang/meta/FromSyntaxTree.class.php +++ b/src/main/php/lang/meta/FromSyntaxTree.class.php @@ -197,10 +197,13 @@ private function annotations($tree, $annotated) { } public function imports($reflect) { - $resolver= $this->tree($reflect)->resolver(); - $imports= []; - foreach ($resolver->imports as $alias => $type) { - $imports[$alias]= ltrim($type, '\\'); + $imports= ['class' => [], 'function' => [], 'const' => []]; + foreach ($this->tree($reflect)->root()->children() as $child) { + if ($child->is('import')) { + foreach ($child->names as $import => $alias) { + $imports[$child->type ?? 'class'][$alias ?? false === ($p= strrpos($import, '\\')) ? $import : substr($import, $p + 1)]= $import; + } + } } return $imports; } diff --git a/src/main/php/lang/meta/SyntaxTree.class.php b/src/main/php/lang/meta/SyntaxTree.class.php index 3d20865..cd735bc 100755 --- a/src/main/php/lang/meta/SyntaxTree.class.php +++ b/src/main/php/lang/meta/SyntaxTree.class.php @@ -15,6 +15,9 @@ public function __construct($tree, $type) { /** @return lang.ast.TypeDeclaration */ public function type() { return $this->type; } + /** @return lang.ast.ParseTree */ + public function root() { return $this->tree; } + public function resolver() { return $this->tree->scope(); } private function resolve($type) { diff --git a/src/main/php/lang/reflection/Member.class.php b/src/main/php/lang/reflection/Member.class.php index 7ce6a1e..ccf4bd5 100755 --- a/src/main/php/lang/reflection/Member.class.php +++ b/src/main/php/lang/reflection/Member.class.php @@ -18,6 +18,9 @@ public function __construct($reflect, $annotations= null) { $this->annotations= $annotations; } + /** Returns type source */ + public function source(): Source { return new Source($this->reflect); } + /** * Returns context for `Type::resolve()` * @@ -32,7 +35,7 @@ public static function resolve($reflect) { '*' => function($type) use($reflect) { $declared= $reflect->getDeclaringClass(); $imports= Reflection::meta()->scopeImports($declared); - return XPClass::forName($imports[$type] ?? $declared->getNamespaceName().'\\'.$type); + return XPClass::forName($imports['class'][$type] ?? $declared->getNamespaceName().'\\'.$type); }, ]; } diff --git a/src/main/php/lang/reflection/Routine.class.php b/src/main/php/lang/reflection/Routine.class.php index dcf0cfa..e1b0304 100755 --- a/src/main/php/lang/reflection/Routine.class.php +++ b/src/main/php/lang/reflection/Routine.class.php @@ -9,6 +9,9 @@ abstract class Routine extends Member { /** @return [:var] */ protected function meta() { return Reflection::meta()->methodAnnotations($this->reflect); } + /** Returns type source */ + public function source(): Source { return new Source($this->reflect); } + /** * Compiles signature * diff --git a/src/main/php/lang/reflection/Source.class.php b/src/main/php/lang/reflection/Source.class.php new file mode 100755 index 0000000..df4a05b --- /dev/null +++ b/src/main/php/lang/reflection/Source.class.php @@ -0,0 +1,75 @@ +reflect= $reflect; + } + + /** @return string */ + public function fileName() { return $this->reflect->getFileName(); } + + /** @return int */ + public function startLine() { return $this->reflect->getStartLine(); } + + /** @return int */ + public function endLine() { return $this->reflect->getEndLine(); } + + /** @return ReflectionClass */ + private function scope() { + return $this->reflect instanceof ReflectionClass ? $this->reflect : $this->reflect->getDeclaringClass(); + } + + /** @return [:string] */ + public function usedTypes() { + return Reflection::meta()->scopeImports($this->scope())['class']; + } + + /** @return [:string] */ + public function usedConstants() { + return Reflection::meta()->scopeImports($this->scope())['const']; + } + + /** @return [:string] */ + public function usedFunctions() { + return Reflection::meta()->scopeImports($this->scope())['function']; + } + + /** @return string */ + public function toString() { + return sprintf( + '%s(file: %s, lines: %d .. %d)', + nameof($this), + $this->reflect->getFileName(), + $this->reflect->getStartLine(), + $this->reflect->getEndLine() + ); + } + + /** @return string */ + public function hashCode() { + return "S{$this->reflect->getFileName()}:{$this->reflect->getStartLine()}-{$this->reflect->getEndLine()}"; + } + + /** + * Comparison + * + * @param var $value + * @return int + */ + public function compareTo($value) { + if ($value instanceof self) { + return Objects::compare( + [$this->reflect->getFileName(), $this->reflect->getStartLine(), $this->reflect->getEndLine()], + [$value->reflect->getFileName(), $value->reflect->getStartLine(), $value->reflect->getEndLine()] + ); + } + return 1; + } +} \ No newline at end of file diff --git a/src/main/php/lang/reflection/Type.class.php b/src/main/php/lang/reflection/Type.class.php index 3162dc5..0c06b75 100755 --- a/src/main/php/lang/reflection/Type.class.php +++ b/src/main/php/lang/reflection/Type.class.php @@ -70,6 +70,9 @@ public function kind(): Kind { } } + /** Returns type source */ + public function source(): Source { return new Source($this->reflect); } + /** * Returns whether a given value is an instance of this type * diff --git a/src/test/php/lang/reflection/unittest/FromAttributesTest.class.php b/src/test/php/lang/reflection/unittest/FromAttributesTest.class.php index 8235639..7e7fc1d 100755 --- a/src/test/php/lang/reflection/unittest/FromAttributesTest.class.php +++ b/src/test/php/lang/reflection/unittest/FromAttributesTest.class.php @@ -6,6 +6,9 @@ use test\{Assert, Test, Ignore as Skip}; use util\Comparison as WithComparison; +use const MODIFIER_PUBLIC; +use function strncmp; + #[Runtime(php: '>=8.0')] class FromAttributesTest { @@ -18,14 +21,18 @@ public function can_create() { public function imports() { Assert::equals( [ - 'ReflectionClass' => null, - 'FromAttributes' => FromAttributes::class, - 'Dynamic' => Dynamic::class, - 'Runtime' => Runtime::class, - 'Assert' => Assert::class, - 'Test' => Test::class, - 'WithComparison' => WithComparison::class, - 'Skip' => Skip::class, + 'const' => ['MODIFIER_PUBLIC' => 'MODIFIER_PUBLIC'], + 'function' => ['strncmp' => 'strncmp'], + 'class' => [ + 'ReflectionClass' => ReflectionClass::class, + 'FromAttributes' => FromAttributes::class, + 'Dynamic' => Dynamic::class, + 'Runtime' => Runtime::class, + 'Assert' => Assert::class, + 'Test' => Test::class, + 'WithComparison' => WithComparison::class, + 'Skip' => Skip::class, + ] ], (new FromAttributes())->imports(new ReflectionClass(self::class)) ); diff --git a/src/test/php/lang/reflection/unittest/SourceTest.class.php b/src/test/php/lang/reflection/unittest/SourceTest.class.php new file mode 100755 index 0000000..fb4a822 --- /dev/null +++ b/src/test/php/lang/reflection/unittest/SourceTest.class.php @@ -0,0 +1,105 @@ +start= $i; + } + $this->end= $i; + } + + #[Test] + public function this_type_source() { + $source= Reflection::type($this)->source(); + + Assert::equals(__FILE__, $source->fileName()); + Assert::equals($this->start, $source->startLine()); + Assert::equals($this->end, $source->endLine()); + } + + #[Test] + public function anonymous_type_source() { + $source= Reflection::type(new class() { })->source(); + $line= __LINE__ - 1; + + Assert::equals(__FILE__, $source->fileName()); + Assert::equals($line, $source->startLine()); + Assert::equals($line, $source->endLine()); + } + + #[Test] + public function defined_type_source() { + $type= ClassLoader::defineType( + 'lang.reflection.unittest.SourceTest_Defined', + ['kind' => 'class', 'extends' => [], 'implements' => [], 'use' => []], + [] + ); + $source= Reflection::type($type)->source(); + + Assert::equals('dyn://lang.reflection.unittest.SourceTest_Defined', $source->fileName()); + Assert::equals(1, $source->startLine()); + Assert::equals(1, $source->endLine()); + } + + #[Test] + public function method_source() { + $source= Reflection::type($this)->method(__FUNCTION__)->source(); + $start= __LINE__ - 2; + $end= __LINE__ + 5; + + Assert::equals(__FILE__, $source->fileName()); + Assert::equals($start, $source->startLine()); + Assert::equals($end, $source->endLine()); + } + + #[Test] + public function trait_method_source() { + $type= ClassLoader::defineType( + 'lang.reflection.unittest.SourceTest_Trait', + ['kind' => 'class', 'extends' => [], 'implements' => [], 'use' => [WithMethod::class]], + [] + ); + $source= Reflection::type($type)->method('fixture')->source(); + + Assert::equals('WithMethod.class.php', basename($source->fileName())); + Assert::equals(5, $source->startLine()); + Assert::equals(7, $source->endLine()); + } + + #[Test] + public function used_classes() { + $resolved= [ + 'File' => File::class, + 'LinesIn' => LinesIn::class, + 'ClassLoader' => ClassLoader::class, + 'Reflection' => Reflection::class, + 'Assert' => Assert::class, + 'Before' => Before::class, + 'Test' => Test::class, + ]; + Assert::equals($resolved, Reflection::type($this)->source()->usedTypes()); + } + + #[Test] + public function used_constants() { + Assert::equals(['MODIFIER_PUBLIC' => 'MODIFIER_PUBLIC'], Reflection::type($this)->source()->usedConstants()); + } + + #[Test] + public function used_functions() { + Assert::equals(['strncmp' => 'strncmp'], Reflection::type($this)->source()->usedFunctions()); + } +} \ No newline at end of file diff --git a/src/test/php/lang/reflection/unittest/WithMethod.class.php b/src/test/php/lang/reflection/unittest/WithMethod.class.php new file mode 100755 index 0000000..9ecb26f --- /dev/null +++ b/src/test/php/lang/reflection/unittest/WithMethod.class.php @@ -0,0 +1,8 @@ +