Skip to content

Commit

Permalink
Support of arrow functions
Browse files Browse the repository at this point in the history
  • Loading branch information
kukulich committed Sep 17, 2021
1 parent 63f5155 commit 12e660b
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 35 deletions.
10 changes: 5 additions & 5 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ parameters:
-
message: '#Call to an undefined method PhpParser\\NodeVisitorAbstract::(getNode|setConstantName)\(\)#'
path: %currentWorkingDirectory%/src/SourceLocator/Type/AutoloadSourceLocator.php
# https://github.com/nikic/PHP-Parser/pull/808
-
message: "#^Property PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\:\\:\\$expr \\(PhpParser\\\\Node\\\\Expr\\) does not accept null\\.$#"
count: 1
path: %currentWorkingDirectory%/src/Reflection/ReflectionFunctionAbstract.php
# Will be fixed later
-
message: "#^Method Roave\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionClass\\:\\:getReflectionConstant\\(\\) should return ReflectionClassConstant but returns false\\.$#"
Expand Down Expand Up @@ -56,8 +61,3 @@ parameters:
message: "#^Method Roave\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionObject\\:\\:isInstance\\(\\) should return bool but returns null\\.$#"
count: 1
path: %currentWorkingDirectory%/src/Reflection/Adapter/ReflectionObject.php

-
message: "#^Property PhpParser\\\\Node\\\\Expr\\\\Closure\\:\\:\\$stmts \\(array\\<PhpParser\\\\Node\\\\Stmt\\>\\|null\\) does not accept array\\<int, PhpParser\\\\Node\\>\\.$#"
count: 1
path: %currentWorkingDirectory%/src/Reflection/ReflectionFunctionAbstract.php
5 changes: 0 additions & 5 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@
<code>public function newInstanceWithoutConstructor()</code>
</MethodSignatureMismatch>
</file>
<file src="src/Reflection/ReflectionFunctionAbstract.php">
<PossiblyNullPropertyAssignmentValue occurrences="1">
<code>$this-&gt;loadStaticParser()-&gt;parse('&lt;?php ' . $newBody)</code>
</PossiblyNullPropertyAssignmentValue>
</file>
<file src="src/Reflection/ReflectionMethod.php">
<InvalidStringClass occurrences="1">
<code>$declaringClassName::{$methodName}(...$methodArgs)</code>
Expand Down
23 changes: 23 additions & 0 deletions src/Reflection/Exception/InvalidArrowFunctionBodyNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflection\Reflection\Exception;

use PhpParser\Node\Stmt;
use PhpParser\PrettyPrinter\Standard;
use RuntimeException;

use function sprintf;
use function substr;

class InvalidArrowFunctionBodyNode extends RuntimeException
{
public static function create(Stmt $node): self
{
return new self(sprintf(
'Invalid arrow function body node (first 50 characters: %s)',
substr((new Standard())->prettyPrint([$node]), 0, 50),
));
}
}
2 changes: 1 addition & 1 deletion src/Reflection/ReflectionFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function __toString(): string
*/
public static function createFromNode(
Reflector $reflector,
Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure $node,
Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node,
LocatedSource $locatedSource,
?NamespaceNode $namespaceNode = null,
): self {
Expand Down
49 changes: 37 additions & 12 deletions src/Reflection/ReflectionFunctionAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Roave\BetterReflection\Identifier\Exception\InvalidIdentifierName;
use Roave\BetterReflection\Identifier\Identifier;
use Roave\BetterReflection\Identifier\IdentifierType;
use Roave\BetterReflection\Reflection\Exception\InvalidArrowFunctionBodyNode;
use Roave\BetterReflection\Reflection\Exception\Uncloneable;
use Roave\BetterReflection\Reflector\Reflector;
use Roave\BetterReflection\SourceLocator\Ast\Exception\ParseToAstFailure;
Expand All @@ -48,7 +49,7 @@ abstract class ReflectionFunctionAbstract

protected function __construct(
private Reflector $reflector,
private Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure $node,
private Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction $node,
private LocatedSource $locatedSource,
private ?NamespaceNode $declaringNamespace = null,
) {
Expand Down Expand Up @@ -96,7 +97,7 @@ public function getName(): string
*/
public function getShortName(): string
{
if ($this->node instanceof Node\Expr\Closure) {
if ($this->node instanceof Node\Expr\Closure || $this->node instanceof Node\Expr\ArrowFunction) {
return self::CLOSURE_NAME;
}

Expand Down Expand Up @@ -212,7 +213,7 @@ public function getLocatedSource(): LocatedSource
*/
public function isClosure(): bool
{
return $this->node instanceof Node\Expr\Closure;
return $this->node instanceof Node\Expr\Closure || $this->node instanceof Node\Expr\ArrowFunction;
}

public function isDeprecated(): bool
Expand Down Expand Up @@ -408,7 +409,7 @@ public function __clone()
*/
public function getBodyAst(): array
{
return $this->node->stmts ?? [];
return $this->node->getStmts() ?? [];
}

/**
Expand All @@ -427,13 +428,20 @@ public function getBodyCode(?PrettyPrinterAbstract $printer = null): string
$printer = new StandardPrettyPrinter();
}

if ($this->node instanceof Node\Expr\ArrowFunction) {
/** @var non-empty-list<Node\Stmt\Return_> $ast */
$ast = $this->getBodyAst();

return $printer->prettyPrintExpr($ast[0]->expr);
}

return $printer->prettyPrint($this->getBodyAst());
}

/**
* Fetch the AST for this method or function.
*/
public function getAst(): Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure
public function getAst(): Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure|Node\Expr\ArrowFunction
{
return $this->node;
}
Expand All @@ -455,9 +463,9 @@ public function setBodyFromClosure(Closure $newBody): void
new Identifier(self::CLOSURE_NAME, new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION)),
);
assert($closureReflection instanceof self);
assert($closureReflection->node instanceof Node\Expr\Closure);
assert($closureReflection->node instanceof Node\Expr\Closure || $closureReflection->node instanceof Node\Expr\ArrowFunction);

$this->node->stmts = $closureReflection->node->getStmts();
$this->setBodyFromAst($closureReflection->node->getStmts());
}

/**
Expand All @@ -469,25 +477,42 @@ public function setBodyFromClosure(Closure $newBody): void
*/
public function setBodyFromString(string $newBody): void
{
$this->node->stmts = $this->loadStaticParser()->parse('<?php ' . $newBody);
$stmts = $this->loadStaticParser()->parse('<?php ' . $newBody);

$this->setBodyFromAst($stmts);
}

/**
* Override the method or function's body of statements with an entirely new
* body of statements within the reflection.
*
* @param Node[] $nodes
* @param list<Node\Stmt> $nodes
*
* @example
* // $ast should be an array of Nodes
* $reflectionFunction->setBodyFromAst($ast);
*/
public function setBodyFromAst(array $nodes): void
{
// This slightly confusing code simply type-checks the $sourceLocators
// This slightly confusing code simply type-checks the $nodes
// array by unpacking them and splatting them in the closure.
$validator = static fn (Node ...$node): array => $node;
$this->node->stmts = $validator(...$nodes);
$validator = static fn (Node\Stmt ...$node): array => $node;
$stmts = $validator(...$nodes);

if ($this->node instanceof Node\Expr\ArrowFunction) {
if ($nodes === []) {
/** @psalm-suppress PossiblyNullPropertyAssignmentValue https://github.com/nikic/PHP-Parser/pull/808 */
$this->node->expr = null;
} elseif (isset($nodes[0]->expr)) {
$this->node->expr = $nodes[0]->expr;
} else {
throw InvalidArrowFunctionBodyNode::create($nodes[0]);
}

return;
}

$this->node->stmts = $stmts;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/SourceLocator/Ast/Strategy/NodeToReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public function __invoke(
$node instanceof Node\Stmt\ClassMethod
|| $node instanceof Node\Stmt\Function_
|| $node instanceof Node\Expr\Closure
|| $node instanceof Node\Expr\ArrowFunction
) {
return ReflectionFunction::createFromNode(
$reflector,
Expand Down
6 changes: 2 additions & 4 deletions src/SourceLocator/Type/ClosureSourceLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,10 @@ public function enterNode(Node $node)
return null;
}

if (! ($node instanceof Node\Expr\Closure)) {
return null;
if ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) {
$this->closureNodes[] = [$node, $this->currentNamespace];
}

$this->closureNodes[] = [$node, $this->currentNamespace];

return null;
}

Expand Down
5 changes: 5 additions & 0 deletions test/unit/Fixture/ArrowFunctionInNamespace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Roave\BetterReflectionTest\Fixture;

return fn () => 'Hello world!';
3 changes: 3 additions & 0 deletions test/unit/Fixture/ArrowFunctionNoNamespace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

return fn () => 'Hello world!';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflectionTest\Reflection\Exception;

use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Echo_;
use PHPUnit\Framework\TestCase;
use Roave\BetterReflection\Reflection\Exception\InvalidArrowFunctionBodyNode;

/**
* @covers \Roave\BetterReflection\Reflection\Exception\InvalidArrowFunctionBodyNode
*/
class InvalidArrowFunctionBodyNodeTest extends TestCase
{
public function testCreate(): void
{
$exception = InvalidArrowFunctionBodyNode::create(new Echo_([
new String_('Hello world!'),
]));

self::assertInstanceOf(InvalidArrowFunctionBodyNode::class, $exception);
self::assertStringStartsWith('Invalid arrow function body node', $exception->getMessage());
}
}
66 changes: 60 additions & 6 deletions test/unit/Reflection/ReflectionFunctionAbstractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use PhpParser\Parser;
use PhpParser\PrettyPrinter\Standard as StandardPrettyPrinter;
use PHPUnit\Framework\TestCase;
use Roave\BetterReflection\Reflection\Exception\InvalidArrowFunctionBodyNode;
use Roave\BetterReflection\Reflection\Exception\Uncloneable;
use Roave\BetterReflection\Reflection\ReflectionFunction;
use Roave\BetterReflection\Reflection\ReflectionFunctionAbstract;
Expand Down Expand Up @@ -589,27 +590,39 @@ public function testSetBodyFromClosure(): void
self::assertSame("echo 'Hello world!';", $function->getBodyCode());
}

public function testSetBodyFromString(): void
public function testSetBodyFromClosureWithArrowFunction(): void
{
$php = '<?php function foo() {}';

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

$function->setBodyFromString("echo 'Hello world!';");
$function->setBodyFromClosure(static fn (): string => 'Hello world!');

self::assertSame("echo 'Hello world!';", $function->getBodyCode());
self::assertSame("return 'Hello world!';", $function->getBodyCode());
}

public function testSetBodyFromAstWithInvalidArgumentsThrowsException(): void
public function testSetBodyFromString(): void
{
$php = '<?php function foo() {}';

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

$this->expectException(TypeError::class);
$function->setBodyFromAst([1]);
$function->setBodyFromString("echo 'Hello world!';");

self::assertSame("echo 'Hello world!';", $function->getBodyCode());
}

public function testSetBodyFromStringForArrowFunction(): void
{
$arrowFunction = static fn () => 10;

$function = ReflectionFunction::createFromClosure($arrowFunction);

$function->setBodyFromString("'Hello world!';");

self::assertSame("'Hello world!'", $function->getBodyCode());
}

public function testSetBodyFromAst(): void
Expand All @@ -628,6 +641,47 @@ public function testSetBodyFromAst(): void
self::assertSame("echo 'Hello world!';", $function->getBodyCode());
}

public function testSetBodyFromAstForArrowFunction(): void
{
$arrowFunction = static fn () => 10;

$function = ReflectionFunction::createFromClosure($arrowFunction);

$function->setBodyFromAst([
new Return_(
new String_('Hello world!'),
),
]);

self::assertSame("'Hello world!'", $function->getBodyCode());
}

public function testSetBodyFromAstWithInvalidArgumentsThrowsException(): void
{
$php = '<?php function foo() {}';

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

$this->expectException(TypeError::class);
$function->setBodyFromAst([1]);
}

public function testSetBodyFromAstForArrowFunctionWithInvalidArgumentsThrowsException(): void
{
$arrowFunction = static fn () => 10;

$function = ReflectionFunction::createFromClosure($arrowFunction);

$this->expectException(InvalidArrowFunctionBodyNode::class);

$function->setBodyFromAst([
new Echo_([
new String_('Hello world!'),
]),
]);
}

public function testAddParameter(): void
{
$php = '<?php function foo() {}';
Expand Down
29 changes: 29 additions & 0 deletions test/unit/Reflection/ReflectionFunctionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,35 @@ public function testCreateFromClosureCanReflectTypesInNamespace(): void
self::assertSame(ClassWithStaticMethod::class, $theParam->getName());
}

public function testCreateFromClosureWithArrowFunction(): void
{
$myClosure = static fn (): int => 5;

$reflection = ReflectionFunction::createFromClosure($myClosure);

self::assertSame(ReflectionFunction::CLOSURE_NAME, $reflection->getShortName());
}

public function testCreateFromClosureWithArrowFunctionCanReflectTypeHints(): void
{
$myClosure = static fn (stdClass $theParam): int => 5;

$reflection = ReflectionFunction::createFromClosure($myClosure);

$theParam = $reflection->getParameter('theParam')->getClass();
self::assertSame(stdClass::class, $theParam->getName());
}

public function testCreateFromClosureWithArrowFunctionCanReflectTypesInNamespace(): void
{
$myClosure = static fn (ClassWithStaticMethod $theParam): int => 5;

$reflection = ReflectionFunction::createFromClosure($myClosure);

$theParam = $reflection->getParameter('theParam')->getClass();
self::assertSame(ClassWithStaticMethod::class, $theParam->getName());
}

public function testToString(): void
{
require_once __DIR__ . '/../Fixture/Functions.php';
Expand Down
Loading

0 comments on commit 12e660b

Please sign in to comment.