From 8193eff15aa37c509d2b8d2952235dc0348e30de Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 12:56:37 +0200 Subject: [PATCH 1/7] Add Source which encapsulates filename, start and end lines for types and methods --- src/main/php/lang/reflection/Member.class.php | 3 + .../php/lang/reflection/Routine.class.php | 3 + src/main/php/lang/reflection/Source.class.php | 54 +++++++++++++++++ src/main/php/lang/reflection/Type.class.php | 3 + .../reflection/unittest/SourceTest.class.php | 60 +++++++++++++++++++ 5 files changed, 123 insertions(+) create mode 100755 src/main/php/lang/reflection/Source.class.php create mode 100755 src/test/php/lang/reflection/unittest/SourceTest.class.php diff --git a/src/main/php/lang/reflection/Member.class.php b/src/main/php/lang/reflection/Member.class.php index 7ce6a1e..fe059a7 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()` * 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..e522123 --- /dev/null +++ b/src/main/php/lang/reflection/Source.class.php @@ -0,0 +1,54 @@ +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 string */ + public function hashCode() { + return "S{$this->reflect->getFileName()}:{$this->reflect->getStartLine()}-{$this->reflect->getEndLine()}"; + } + + /** @return string */ + public function toString() { + return sprintf( + '%s(file: %s, lines: %d .. %d)', + nameof($this), + $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/SourceTest.class.php b/src/test/php/lang/reflection/unittest/SourceTest.class.php new file mode 100755 index 0000000..62d4d06 --- /dev/null +++ b/src/test/php/lang/reflection/unittest/SourceTest.class.php @@ -0,0 +1,60 @@ +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::defineClass('lang.reflection.unittest.SourceTest_Defined', null, [], []); + $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()); + } +} \ No newline at end of file From 0b935fd15fb0cd4783ccf6a6a5167eb4af84641c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 16:14:54 +0200 Subject: [PATCH 2/7] Make imports() return classes, functions and constants --- .../php/lang/meta/FromAttributes.class.php | 35 +++++++++++++------ .../php/lang/meta/FromSyntaxTree.class.php | 11 +++--- src/main/php/lang/meta/SyntaxTree.class.php | 3 ++ src/main/php/lang/reflection/Member.class.php | 2 +- .../unittest/FromAttributesTest.class.php | 23 +++++++----- 5 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/main/php/lang/meta/FromAttributes.class.php b/src/main/php/lang/meta/FromAttributes.class.php index 2f58229..d3415db 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]= null; } 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 fe059a7..ccf4bd5 100755 --- a/src/main/php/lang/reflection/Member.class.php +++ b/src/main/php/lang/reflection/Member.class.php @@ -35,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/test/php/lang/reflection/unittest/FromAttributesTest.class.php b/src/test/php/lang/reflection/unittest/FromAttributesTest.class.php index 8235639..0882dbe 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' => null], + 'function' => ['strncmp' => null], + 'class' => [ + 'ReflectionClass' => null, + '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)) ); From e440127d02c8ba61143512dda52dcf405b3b7b20 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 17:19:18 +0200 Subject: [PATCH 3/7] Add test for trait methods' source --- .../reflection/unittest/SourceTest.class.php | 20 ++++++++++++++++++- .../reflection/unittest/WithMethod.class.php | 8 ++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100755 src/test/php/lang/reflection/unittest/WithMethod.class.php diff --git a/src/test/php/lang/reflection/unittest/SourceTest.class.php b/src/test/php/lang/reflection/unittest/SourceTest.class.php index 62d4d06..f189e91 100755 --- a/src/test/php/lang/reflection/unittest/SourceTest.class.php +++ b/src/test/php/lang/reflection/unittest/SourceTest.class.php @@ -39,7 +39,11 @@ public function anonymous_type_source() { #[Test] public function defined_type_source() { - $type= ClassLoader::defineClass('lang.reflection.unittest.SourceTest_Defined', null, [], []); + $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()); @@ -57,4 +61,18 @@ public function method_source() { 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()); + } } \ 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 @@ + Date: Sun, 30 Jul 2023 17:23:45 +0200 Subject: [PATCH 4/7] Expose imports --- src/main/php/lang/reflection/Source.class.php | 11 +++++++- .../reflection/unittest/SourceTest.class.php | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/reflection/Source.class.php b/src/main/php/lang/reflection/Source.class.php index e522123..e51225a 100755 --- a/src/main/php/lang/reflection/Source.class.php +++ b/src/main/php/lang/reflection/Source.class.php @@ -1,6 +1,7 @@ reflect->getFileName()}:{$this->reflect->getStartLine()}-{$this->reflect->getEndLine()}"; } + /** @return [:[:string]] */ + public function imports() { + return Reflection::meta()->scopeImports($this->reflect instanceof ReflectionClass + ? $this->reflect + : $this->reflect->getDeclaringClass() + ); + } + /** @return string */ public function toString() { return sprintf( diff --git a/src/test/php/lang/reflection/unittest/SourceTest.class.php b/src/test/php/lang/reflection/unittest/SourceTest.class.php index f189e91..bb4e9e8 100755 --- a/src/test/php/lang/reflection/unittest/SourceTest.class.php +++ b/src/test/php/lang/reflection/unittest/SourceTest.class.php @@ -5,6 +5,9 @@ use lang\{ClassLoader, Reflection}; use test\{Assert, Before, Test}; +use const MODIFIER_PUBLIC; +use function strncmp; + class SourceTest { private $start, $end; @@ -75,4 +78,28 @@ public function trait_method_source() { Assert::equals(5, $source->startLine()); Assert::equals(7, $source->endLine()); } + + #[Test] + public function class_imports() { + $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()->imports()['class']); + } + + #[Test] + public function const_imports() { + Assert::equals(['MODIFIER_PUBLIC' => null], Reflection::type($this)->source()->imports()['const']); + } + + #[Test] + public function function_imports() { + Assert::equals(['strncmp' => null], Reflection::type($this)->source()->imports()['function']); + } } \ No newline at end of file From df8d62d8544c560ff1e125173f23d2824e5c0582 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 17:31:35 +0200 Subject: [PATCH 5/7] Make PHP 7 and PHP 8 imports consistent --- src/main/php/lang/meta/FromAttributes.class.php | 2 +- .../lang/reflection/unittest/FromAttributesTest.class.php | 6 +++--- src/test/php/lang/reflection/unittest/SourceTest.class.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/meta/FromAttributes.class.php b/src/main/php/lang/meta/FromAttributes.class.php index d3415db..affd739 100755 --- a/src/main/php/lang/meta/FromAttributes.class.php +++ b/src/main/php/lang/meta/FromAttributes.class.php @@ -113,7 +113,7 @@ public function imports($reflect) { $i+= 2; $imports[$kind][$tokens[$i]->text]= $type; } else if (false === ($p= strrpos($type, '\\'))) { - $imports[$kind][$type]= null; + $imports[$kind][$type]= $type; } else { $imports[$kind][substr($type, strrpos($type, '\\') + 1)]= $type; } diff --git a/src/test/php/lang/reflection/unittest/FromAttributesTest.class.php b/src/test/php/lang/reflection/unittest/FromAttributesTest.class.php index 0882dbe..7e7fc1d 100755 --- a/src/test/php/lang/reflection/unittest/FromAttributesTest.class.php +++ b/src/test/php/lang/reflection/unittest/FromAttributesTest.class.php @@ -21,10 +21,10 @@ public function can_create() { public function imports() { Assert::equals( [ - 'const' => ['MODIFIER_PUBLIC' => null], - 'function' => ['strncmp' => null], + 'const' => ['MODIFIER_PUBLIC' => 'MODIFIER_PUBLIC'], + 'function' => ['strncmp' => 'strncmp'], 'class' => [ - 'ReflectionClass' => null, + 'ReflectionClass' => ReflectionClass::class, 'FromAttributes' => FromAttributes::class, 'Dynamic' => Dynamic::class, 'Runtime' => Runtime::class, diff --git a/src/test/php/lang/reflection/unittest/SourceTest.class.php b/src/test/php/lang/reflection/unittest/SourceTest.class.php index bb4e9e8..ea27019 100755 --- a/src/test/php/lang/reflection/unittest/SourceTest.class.php +++ b/src/test/php/lang/reflection/unittest/SourceTest.class.php @@ -95,11 +95,11 @@ public function class_imports() { #[Test] public function const_imports() { - Assert::equals(['MODIFIER_PUBLIC' => null], Reflection::type($this)->source()->imports()['const']); + Assert::equals(['MODIFIER_PUBLIC' => 'MODIFIER_PUBLIC'], Reflection::type($this)->source()->imports()['const']); } #[Test] public function function_imports() { - Assert::equals(['strncmp' => null], Reflection::type($this)->source()->imports()['function']); + Assert::equals(['strncmp' => 'strncmp'], Reflection::type($this)->source()->imports()['function']); } } \ No newline at end of file From a0daff696eb9c43244f1957b50ae2d6f4583f9ed Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 18:54:42 +0200 Subject: [PATCH 6/7] Refactor imports() -> used(Types|Functions|Constants)() --- src/main/php/lang/reflection/Source.class.php | 24 ++++++++++++++----- .../reflection/unittest/SourceTest.class.php | 12 +++++----- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/main/php/lang/reflection/Source.class.php b/src/main/php/lang/reflection/Source.class.php index e51225a..3ca84ea 100755 --- a/src/main/php/lang/reflection/Source.class.php +++ b/src/main/php/lang/reflection/Source.class.php @@ -26,12 +26,24 @@ public function hashCode() { return "S{$this->reflect->getFileName()}:{$this->reflect->getStartLine()}-{$this->reflect->getEndLine()}"; } - /** @return [:[:string]] */ - public function imports() { - return Reflection::meta()->scopeImports($this->reflect instanceof ReflectionClass - ? $this->reflect - : $this->reflect->getDeclaringClass() - ); + /** @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 */ diff --git a/src/test/php/lang/reflection/unittest/SourceTest.class.php b/src/test/php/lang/reflection/unittest/SourceTest.class.php index ea27019..fb4a822 100755 --- a/src/test/php/lang/reflection/unittest/SourceTest.class.php +++ b/src/test/php/lang/reflection/unittest/SourceTest.class.php @@ -80,7 +80,7 @@ public function trait_method_source() { } #[Test] - public function class_imports() { + public function used_classes() { $resolved= [ 'File' => File::class, 'LinesIn' => LinesIn::class, @@ -90,16 +90,16 @@ public function class_imports() { 'Before' => Before::class, 'Test' => Test::class, ]; - Assert::equals($resolved, Reflection::type($this)->source()->imports()['class']); + Assert::equals($resolved, Reflection::type($this)->source()->usedTypes()); } #[Test] - public function const_imports() { - Assert::equals(['MODIFIER_PUBLIC' => 'MODIFIER_PUBLIC'], Reflection::type($this)->source()->imports()['const']); + public function used_constants() { + Assert::equals(['MODIFIER_PUBLIC' => 'MODIFIER_PUBLIC'], Reflection::type($this)->source()->usedConstants()); } #[Test] - public function function_imports() { - Assert::equals(['strncmp' => 'strncmp'], Reflection::type($this)->source()->imports()['function']); + public function used_functions() { + Assert::equals(['strncmp' => 'strncmp'], Reflection::type($this)->source()->usedFunctions()); } } \ No newline at end of file From 3dfaf2f59b070c2d7719fe57984367bb3f3d70ff Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 19:00:59 +0200 Subject: [PATCH 7/7] Reorder methods --- src/main/php/lang/reflection/Source.class.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/reflection/Source.class.php b/src/main/php/lang/reflection/Source.class.php index 3ca84ea..df4a05b 100755 --- a/src/main/php/lang/reflection/Source.class.php +++ b/src/main/php/lang/reflection/Source.class.php @@ -21,11 +21,6 @@ public function startLine() { return $this->reflect->getStartLine(); } /** @return int */ public function endLine() { return $this->reflect->getEndLine(); } - /** @return string */ - public function hashCode() { - return "S{$this->reflect->getFileName()}:{$this->reflect->getStartLine()}-{$this->reflect->getEndLine()}"; - } - /** @return ReflectionClass */ private function scope() { return $this->reflect instanceof ReflectionClass ? $this->reflect : $this->reflect->getDeclaringClass(); @@ -57,6 +52,11 @@ public function toString() { ); } + /** @return string */ + public function hashCode() { + return "S{$this->reflect->getFileName()}:{$this->reflect->getStartLine()}-{$this->reflect->getEndLine()}"; + } + /** * Comparison *