Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Stringable interface #770

Merged
merged 1 commit into from
Sep 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions src/Reflection/ReflectionClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use Roave\BetterReflection\SourceLocator\Located\LocatedSource;
use Roave\BetterReflection\Util\CalculateReflectionColumn;
use Roave\BetterReflection\Util\GetLastDocComment;
use Stringable;
use Traversable;

use function array_combine;
Expand Down Expand Up @@ -950,6 +951,27 @@ public function getTraits(): array
);
}

/**
* @param array<class-string, self> $interfaces
*
* @return array<class-string, self>
*/
private function addStringableInterface(array $interfaces): array
{
if (array_key_exists(Stringable::class, $interfaces)) {
return $interfaces;
}

foreach ($this->node->getMethods() as $methodNode) {
if ($methodNode->name->toLowerString() === '__tostring') {
$interfaces[Stringable::class] = $this->reflectClassForNamedNode(new Node\Name(Stringable::class));
break;
}
}

return $interfaces;
}

/**
* Given an AST Node\Name, create a new ReflectionClass for the element.
*/
Expand Down Expand Up @@ -1276,14 +1298,14 @@ public function isIterateable(): bool
}

/**
* @return array<string, ReflectionClass>
* @return array<class-string, ReflectionClass>
*/
private function getCurrentClassImplementedInterfacesIndexedByName(): array
{
$node = $this->node;

if ($node instanceof ClassNode) {
return array_merge(
$interfaces = array_merge(
[],
...array_map(
fn (Node\Name $interfaceName): array => $this
Expand All @@ -1292,6 +1314,8 @@ private function getCurrentClassImplementedInterfacesIndexedByName(): array
$node->implements,
),
);

return $this->addStringableInterface($interfaces);
}

// assumption: first key is the current interface
Expand All @@ -1315,7 +1339,7 @@ private function getInheritanceClassHierarchy(): array
/**
* This method allows us to retrieve all interfaces parent of the this interface. Do not use on class nodes!
*
* @return array<string, ReflectionClass> parent interfaces of this interface
* @return array<class-string, ReflectionClass> parent interfaces of this interface
*
* @throws NotAnInterfaceReflection
*/
Expand All @@ -1328,7 +1352,8 @@ private function getInterfacesHierarchy(): array
$node = $this->node;
assert($node instanceof InterfaceNode);

return array_merge(
/** @var array<class-string, self> $interfaces */
$interfaces = array_merge(
[$this->getName() => $this],
...array_map(
fn (Node\Name $interfaceName): array => $this
Expand All @@ -1337,6 +1362,8 @@ private function getInterfacesHierarchy(): array
$node->extends,
),
);

return $this->addStringableInterface($interfaces);
}

/**
Expand Down
71 changes: 71 additions & 0 deletions test/unit/Reflection/ReflectionClassTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Roave\BetterReflection\Reflector\ClassReflector;
use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound;
use Roave\BetterReflection\SourceLocator\Ast\Locator;
use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\ComposerSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\SingleFileSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\StringSourceLocator;
Expand Down Expand Up @@ -2004,4 +2005,74 @@ class Foo
$privateMethod = $reflection->getMethod('privateMethodRenamed');
self::assertTrue($privateMethod->isProtected());
}

public function testHasStringableInterface(): void
{
$php = <<<'PHP'
<?php

class ClassHasStringable implements Stringable
{
public function __toString(): string
{
}
}

class ClassHasStringableAutomatically
{
public function __toString(): string
{
}
}

interface InterfaceHasStringable extends \Stringable
{
}

interface InterfaceHasStringableAutomatically
{
public function __toString();
}
PHP;

$reflector = new ClassReflector(new AggregateSourceLocator([
new StringSourceLocator($php, $this->astLocator),
BetterReflectionSingleton::instance()->sourceLocator(),
]));

$classImplementingStringable = $reflector->reflect('ClassHasStringable');
self::assertContains('Stringable', $classImplementingStringable->getInterfaceNames());

$classNotImplementingStringable = $reflector->reflect('ClassHasStringableAutomatically');
self::assertContains('Stringable', $classNotImplementingStringable->getInterfaceNames());

$interfaceExtendingStringable = $reflector->reflect('InterfaceHasStringable');
self::assertContains('Stringable', $interfaceExtendingStringable->getInterfaceNames());

$interfaceNotExtendingStringable = $reflector->reflect('InterfaceHasStringableAutomatically');
self::assertContains('Stringable', $interfaceNotExtendingStringable->getInterfaceNames());
}

public function testHasAllInterfacesWithStringable(): void
{
$php = <<<'PHP'
<?php

abstract class HasStringable implements Iterator
{
public function __toString(): string
{
}
}
PHP;

$reflector = new ClassReflector(new AggregateSourceLocator([
new StringSourceLocator($php, $this->astLocator),
BetterReflectionSingleton::instance()->sourceLocator(),
]));

$class = $reflector->reflect('HasStringable');

self::assertSame(['Iterator', 'Traversable', 'Stringable'], $class->getInterfaceNames());
}
}