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

Discover return types for functions and methods #104

Merged
merged 2 commits into from
Aug 20, 2015
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
15 changes: 15 additions & 0 deletions src/Reflection/ReflectionFunctionAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

use BetterReflection\Reflector\Reflector;
use BetterReflection\SourceLocator\LocatedSource;
use BetterReflection\TypesFinder\FindReturnType;
use PhpParser\Node;
use PhpParser\Node\Stmt as MethodOrFunctionNode;
use PhpParser\Node\Stmt\Namespace_ as NamespaceNode;
use PhpParser\Node\Expr\Yield_ as YieldNode;
use phpDocumentor\Reflection\Type;

abstract class ReflectionFunctionAbstract implements \Reflector
{
Expand Down Expand Up @@ -380,4 +382,17 @@ public function returnsReference()
{
return (bool)$this->node->byRef;
}

/**
* Get the return types defined in the DocBlocks. This returns an array because
* the parameter may have multiple (compound) types specified (for example
* when you type hint pipe-separated "string|null", in which case this
* would return an array of Type objects, one for string, one for null.
*
* @return Type[]
*/
public function getDocblockReturnTypes()
{
return (new FindReturnType())->__invoke($this);
}
}
1 change: 0 additions & 1 deletion src/Reflection/ReflectionParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use BetterReflection\NodeCompiler\CompilerContext;
use BetterReflection\Reflector\ClassReflector;
use BetterReflection\Reflector\Reflector;
use BetterReflection\NodeCompiler\Exception\UnableToCompileNode;
use BetterReflection\TypesFinder\FindParameterType;
use BetterReflection\TypesFinder\FindTypeFromAst;
use phpDocumentor\Reflection\Types;
Expand Down
54 changes: 54 additions & 0 deletions src/TypesFinder/FindReturnType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace BetterReflection\TypesFinder;

use BetterReflection\Reflection\ReflectionMethod;
use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\ContextFactory;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\Type;
use BetterReflection\Reflection\ReflectionFunctionAbstract;

class FindReturnType
{
/**
* Given a function, attempt to find the return type.
*
* @param ReflectionFunctionAbstract $function
* @return Type[]
*/
public function __invoke(ReflectionFunctionAbstract $function)
{
$context = $this->createContextForFunction($function);

$returnTags = (new DocBlock(
$function->getDocComment(),
new DocBlock\Context(
$context->getNamespace(),
$context->getNamespaceAliases()
)
))->getTagsByName('return');

foreach ($returnTags as $returnTag) {
/* @var $returnTag \phpDocumentor\Reflection\DocBlock\Tag\ReturnTag */
return (new ResolveTypes())->__invoke($returnTag->getTypes(), $context);
}
return [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be mixed by default?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, I don't think we should make any assumptions about return types. If they're there, we return them, if not, we don't. Plain and simple that way - if applications using Better Refletion want to make that assumption, they can do so simply.

}

/**
* @param ReflectionFunctionAbstract $function
* @return Context
*/
private function createContextForFunction(ReflectionFunctionAbstract $function)
{
if ($function instanceof ReflectionMethod) {
$function = $function->getDeclaringClass();
}

return (new ContextFactory())->createForNamespace(
$function->getNamespaceName(),
$function->getLocatedSource()->getSource()
);
}
}
19 changes: 19 additions & 0 deletions test/unit/Reflection/ReflectionFunctionAbstractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use BetterReflection\SourceLocator\LocatedSource;
use BetterReflection\SourceLocator\SingleFileSourceLocator;
use BetterReflection\SourceLocator\StringSourceLocator;
use phpDocumentor\Reflection\Types\Boolean;
use PhpParser\Node\Stmt\Function_;

/**
Expand Down Expand Up @@ -276,4 +277,22 @@ public function testGetLocatedSource()

$this->assertSame($locatedSource, $functionInfo->getLocatedSource());
}

public function testGetDocblockReturnTypes()
{
$php = '<?php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use <<<'PHP'

/**
* @return bool
*/
function foo() {}';

$reflector = new FunctionReflector(new StringSourceLocator($php));
$function = $reflector->reflect('foo');

$types = $function->getDocblockReturnTypes();

$this->assertInternalType('array', $types);
$this->assertCount(1, $types);
$this->assertInstanceOf(Boolean::class, $types[0]);
}
}
19 changes: 19 additions & 0 deletions test/unit/Reflection/ReflectionFunctionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use BetterReflection\Reflection\ReflectionFunction;
use BetterReflection\Reflector\FunctionReflector;
use BetterReflection\SourceLocator\StringSourceLocator;
use phpDocumentor\Reflection\Types\Boolean;

/**
* @covers \BetterReflection\Reflection\ReflectionFunction
Expand Down Expand Up @@ -107,4 +108,22 @@ public function testStringCast($functionName, $expectedStringValue)

$this->assertStringMatchesFormat($expectedStringValue, (string)$functionInfo);
}

public function testGetDocblockReturnTypes()
{
$php = '<?php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use <<<'PHP'

/**
* @return bool
*/
function foo() {}';

$reflector = new FunctionReflector(new StringSourceLocator($php));
$function = $reflector->reflect('foo');

$types = $function->getDocblockReturnTypes();

$this->assertInternalType('array', $types);
$this->assertCount(1, $types);
$this->assertInstanceOf(Boolean::class, $types[0]);
}
}
25 changes: 25 additions & 0 deletions test/unit/Reflection/ReflectionMethodTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
use BetterReflection\Reflection\ReflectionParameter;
use BetterReflection\SourceLocator\ComposerSourceLocator;
use BetterReflection\SourceLocator\SingleFileSourceLocator;
use BetterReflection\SourceLocator\StringSourceLocator;
use BetterReflectionTest\Fixture\ExampleClass;
use phpDocumentor\Reflection\Types\Integer;
use PhpParser\Node\Stmt\Function_;

/**
Expand Down Expand Up @@ -142,6 +145,28 @@ public function testMethodNameWithNamespace()
$this->assertSame('someMethod', $methodInfo->getShortName());
}

public function testGetDocblockReturnTypes()
{
$php = '<?php
class Foo {
/**
* @return int
*/
public function someMethod() {}
}
';

$methodInfo = (new ClassReflector(new StringSourceLocator($php)))
->reflect('Foo')
->getMethod('someMethod');

$types = $methodInfo->getDocblockReturnTypes();

$this->assertInternalType('array', $types);
$this->assertCount(1, $types);
$this->assertInstanceOf(Integer::class, $types[0]);
}

public function modifierProvider()
{
return [
Expand Down
109 changes: 109 additions & 0 deletions test/unit/TypesFinder/FindReturnTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace BetterReflectionTest\TypesFinder;

use BetterReflection\Reflection\ReflectionClass;
use BetterReflection\Reflection\ReflectionFunction;
use BetterReflection\Reflection\ReflectionMethod;
use BetterReflection\SourceLocator\LocatedSource;
use BetterReflection\TypesFinder\FindReturnType;
use phpDocumentor\Reflection\Types;

/**
* @covers \BetterReflection\TypesFinder\FindReturnType
*/
class FindReturnTypeTest extends \PHPUnit_Framework_TestCase
{
/**
* @return array
*/
public function returnTypeProvider()
{
return [
['@return int|string', [Types\Integer::class, Types\String_::class]],
['@return array', [Types\Array_::class]],
['@return \stdClass', [Types\Object_::class]],
['@return int|int[]|int[][]', [Types\Integer::class, Types\Array_::class, Types\Array_::class]],
['@return int A comment about the return type', [Types\Integer::class]],
['', []],
];
}

/**
* @param string $docBlock
* @param string[] $expectedInstances
* @dataProvider returnTypeProvider
*/
public function testFindReturnTypeForFunction($docBlock, $expectedInstances)
{
$docBlock = "/**\n * $docBlock\n */";

$function = $this->getMockBuilder(ReflectionFunction::class)
->setMethods(['getDocComment', 'getLocatedSource'])
->disableOriginalConstructor()
->getMock();

$function
->expects($this->once())
->method('getDocComment')
->will($this->returnValue($docBlock));

$function
->expects($this->once())
->method('getLocatedSource')
->will($this->returnValue(new LocatedSource('<?php', null)));

/* @var ReflectionFunction $function */
$foundTypes = (new FindReturnType())->__invoke($function);

$this->assertCount(count($expectedInstances), $foundTypes);

foreach ($expectedInstances as $i => $expectedInstance) {
$this->assertInstanceOf($expectedInstance, $foundTypes[$i]);
}
}

/**
* @param string $docBlock
* @param string[] $expectedInstances
* @dataProvider returnTypeProvider
*/
public function testFindReturnTypeForMethod($docBlock, $expectedInstances)
{
$docBlock = "/**\n * $docBlock\n */";

$class = $this->getMockBuilder(ReflectionClass::class)
->setMethods(['getLocatedSource'])
->disableOriginalConstructor()
->getMock();

$class
->expects($this->once())
->method('getLocatedSource')
->will($this->returnValue(new LocatedSource('<?php', null)));

$method = $this->getMockBuilder(ReflectionMethod::class)
->setMethods(['getDocComment', 'getDeclaringClass'])
->disableOriginalConstructor()
->getMock();

$method
->expects($this->once())
->method('getDocComment')
->will($this->returnValue($docBlock));

$method
->expects($this->once())
->method('getDeclaringClass')
->will($this->returnValue($class));

/* @var ReflectionMethod $method */
$foundTypes = (new FindReturnType())->__invoke($method);

$this->assertCount(count($expectedInstances), $foundTypes);

foreach ($expectedInstances as $i => $expectedInstance) {
$this->assertInstanceOf($expectedInstance, $foundTypes[$i]);
}
}
}