Skip to content

Commit

Permalink
refactored cleaning parser logic
Browse files Browse the repository at this point in the history
  • Loading branch information
sebkehr committed Jan 30, 2025
1 parent 07875ff commit d446d6c
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 65 deletions.
192 changes: 131 additions & 61 deletions src/Parser/CleaningVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,102 +3,172 @@
namespace PHPStan\Parser;

use PhpParser\Node;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\ShouldNotHappenException;
use function array_filter;
use function array_map;
use function in_array;
use function is_array;

final class CleaningVisitor extends NodeVisitorAbstract
{

private NodeFinder $nodeFinder;
private const CONTEXT_DEFAULT = 0;

public function __construct()
private const CONTEXT_FUNCTION_OR_METHOD = 1;

private const CONTEXT_PROPERTY_HOOK = 2;

/** @var self::CONTEXT_* */
private int $context = self::CONTEXT_DEFAULT;

private string|null $propertyName = null;

/**
* @return int|Node[]|null
*/
public function enterNode(Node $node): int|array|null
{
$this->nodeFinder = new NodeFinder();
switch ($this->context) {
case self::CONTEXT_DEFAULT:
return $this->clean($node);
case self::CONTEXT_FUNCTION_OR_METHOD:
return $this->cleanFunctionOrMethod($node);
case self::CONTEXT_PROPERTY_HOOK:
return $this->cleanPropertyHook($node);
}
}

public function enterNode(Node $node): ?Node
private function clean(Node $node): int|null
{
if ($node instanceof Node\Stmt\Function_) {
$node->stmts = $this->keepVariadicsAndYields($node->stmts, null);
return $node;
}
if (($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) && $node->stmts !== null) {
$params = [];
foreach ($this->traverse($node->params, self::CONTEXT_DEFAULT) as $param) {
if (!($param instanceof Node\Param)) {
continue;
}

if ($node instanceof Node\Stmt\ClassMethod && $node->stmts !== null) {
$node->stmts = $this->keepVariadicsAndYields($node->stmts, null);
return $node;
}
$params[] = $param;
}
$node->params = $params;

if ($node instanceof Node\Expr\Closure) {
$node->stmts = $this->keepVariadicsAndYields($node->stmts, null);
return $node;
$stmts = [];
foreach ($this->traverse($node->stmts, self::CONTEXT_FUNCTION_OR_METHOD) as $stmt) {
if (!($stmt instanceof Node\Stmt)) {
continue;
}

$stmts[] = $stmt;
}
$node->stmts = $stmts;

return self::DONT_TRAVERSE_CHILDREN;
}

if ($node instanceof Node\PropertyHook && is_array($node->body)) {
$propertyName = $node->getAttribute('propertyName');
if ($propertyName !== null) {
$node->body = $this->keepVariadicsAndYields($node->body, $propertyName);
return $node;
$body = [];
foreach ($this->traverse($node->body, self::CONTEXT_PROPERTY_HOOK, $propertyName) as $stmt) {
if (!($stmt instanceof Node\Stmt)) {
continue;
}

$body[] = $stmt;
}
$node->body = $body;

return self::DONT_TRAVERSE_CHILDREN;
}
}

return null;
}

/**
* @param Node\Stmt[] $stmts
* @return Node\Stmt[]
* @return int|Node[]
*/
private function keepVariadicsAndYields(array $stmts, ?string $hookedPropertyName): array
private function cleanFunctionOrMethod(Node $node): int|array
{
$results = $this->nodeFinder->find($stmts, static function (Node $node) use ($hookedPropertyName): bool {
if ($node instanceof Node\Expr\YieldFrom || $node instanceof Node\Expr\Yield_) {
return true;
}
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) {
return in_array($node->name->toLowerString(), ParametersAcceptor::VARIADIC_FUNCTIONS, true);
}
if ($node instanceof Node\Expr\YieldFrom || $node instanceof Node\Expr\Yield_) {
return self::DONT_TRAVERSE_CHILDREN;
}

if ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) {
return true;
}
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name
&& in_array($node->name->toLowerString(), ParametersAcceptor::VARIADIC_FUNCTIONS, true)
) {
$node->name = new Node\Name\FullyQualified('func_get_args');
return self::DONT_TRAVERSE_CHILDREN;
}

if ($hookedPropertyName !== null) {
if (
$node instanceof Node\Expr\PropertyFetch
&& $node->var instanceof Node\Expr\Variable
&& $node->var->name === 'this'
&& $node->name instanceof Node\Identifier
&& $node->name->toString() === $hookedPropertyName
) {
return true;
}
}
if ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) {
return self::REMOVE_NODE;
}

return false;
});
$newStmts = [];
foreach ($results as $result) {
if (
$result instanceof Node\Expr\Yield_
|| $result instanceof Node\Expr\YieldFrom
|| $result instanceof Node\Expr\Closure
|| $result instanceof Node\Expr\ArrowFunction
|| $result instanceof Node\Expr\PropertyFetch
) {
$newStmts[] = new Node\Stmt\Expression($result);
continue;
}
if (!$result instanceof Node\Expr\FuncCall) {
continue;
}
return $this->cleanSubnodes($node);
}

/**
* @param Node[] $nodes
* @param self::CONTEXT_* $context
* @return Node[]
*/
private function traverse(
array $nodes,
int $context = self::CONTEXT_DEFAULT,
string|null $propertyName = null,
): array
{
$visitor = new self();
$visitor->context = $context;
$visitor->propertyName = $propertyName;

return (new NodeTraverser($visitor))->traverse($nodes);
}

$newStmts[] = new Node\Stmt\Expression(new Node\Expr\FuncCall(new Node\Name\FullyQualified('func_get_args')));
/**
* @return Node[]
*/
private function cleanPropertyHook(Node $node): int|array
{
if (
$node instanceof Node\Expr\PropertyFetch
&& $node->var instanceof Node\Expr\Variable
&& $node->var->name === 'this'
&& $node->name instanceof Node\Identifier
&& $node->name->toString() === $this->propertyName
) {
return self::DONT_TRAVERSE_CHILDREN;
}

Check failure on line 145 in src/Parser/CleaningVisitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, ubuntu-latest)

Method PHPStan\Parser\CleaningVisitor::cleanPropertyHook() should return array<PhpParser\Node> but returns int.

Check failure on line 145 in src/Parser/CleaningVisitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, windows-latest)

Method PHPStan\Parser\CleaningVisitor::cleanPropertyHook() should return array<PhpParser\Node> but returns int.
return $newStmts;
return $this->cleanSubnodes($node);
}

/**
* @return Node[]
*/
private function cleanSubnodes(Node $node): array
{
$subnodes = [];
foreach ($node->getSubNodeNames() as $subnodeName) {
$subnodes = [...$subnodes, ...array_filter(
is_array($node->$subnodeName) ? $node->$subnodeName : [$node->$subnodeName],
static fn ($subnode) => $subnode instanceof Node,
)];
}

return array_map(static function ($node) {
switch (true) {
case $node instanceof Node\Stmt:
return $node;
case $node instanceof Node\Expr:
return new Node\Stmt\Expression($node);
default:
throw new ShouldNotHappenException();
}
}, $this->traverse($subnodes, $this->context, $this->propertyName));
}

}
11 changes: 7 additions & 4 deletions tests/PHPStan/Parser/data/cleaning-1-after.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public function someGenerator2()
{
yield from [1, 2, 3];
}
public function someGenerator3()
{
yield;
}
public function someVariadics()
{
\func_get_args();
Expand All @@ -43,9 +47,8 @@ class ContainsClosure
{
public function doFoo()
{
static function () {
yield;
};
yield;
}
public function doBar()
{
}
}
10 changes: 10 additions & 0 deletions tests/PHPStan/Parser/data/cleaning-1-before.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public function someGenerator2()
}
}

public function someGenerator3()
{
echo yield;
}

public function someVariadics()
{
if (rand(0, 1)) {
Expand Down Expand Up @@ -82,4 +87,9 @@ public function doFoo()
};
}

public function doBar()
{
$fn = fn() => yield;
}

}

0 comments on commit d446d6c

Please sign in to comment.