improve CreateFunctionToAnonymousFunctionRector

This commit is contained in:
Tomas Votruba 2018-12-15 22:05:44 +01:00
parent 6ed11cc2ef
commit 605419a0d4
10 changed files with 383 additions and 36 deletions

View File

@ -4,10 +4,15 @@ namespace Rector\Php\Rector\FuncCall;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\ClosureUse;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\Encapsed;
use PhpParser\Node\Scalar\String_;
@ -89,12 +94,12 @@ CODE_SAMPLE
}
/** @var Variable[] $parameters */
$parameters = $this->parseStringToNodes($node->args[0]->value);
$body = $this->parseStringToNodes($node->args[1]->value);
$parameters = $this->parseStringToParameters($node->args[0]->value);
$body = $this->parseStringToBody($node->args[1]->value);
$useVariables = $this->resolveUseVariables($body, $parameters);
$anonymousFunctionNode = new Closure();
foreach ($parameters as $parameter) {
/** @var Variable $parameter */
$anonymousFunctionNode->params[] = new Param($parameter);
@ -112,41 +117,34 @@ CODE_SAMPLE
}
/**
* @param string|String_|Node $content
* @return Node[]|Variable[]|Stmt[]
* @param String_|Expr $content
* @return Stmt[]
*/
private function parseStringToNodes($content): array
private function parseStringToBody(Node $content): array
{
if ($content instanceof String_) {
$content = $content->value;
if (! $content instanceof String_ && ! $content instanceof Encapsed && ! $content instanceof Concat) {
// special case of code elsewhere
return [$this->createEval($content)];
}
if ($content instanceof Encapsed) {
// remove "
$content = trim($this->print($content), '""');
// use \$ → $
$content = Strings::replace($content, '#\\\\\$#', '$');
// use \'{$...}\' → $...
$content = Strings::replace($content, '#\'{(\$.*?)}\'#', '$1');
}
$content = $this->stringify($content);
$content = Strings::endsWith($content, ';') ? $content : $content . ';';
if (! is_string($content)) {
throw new ShouldNotHappenException();
}
return (array) $this->parser->parse('<?php ' . $content);
}
$wrappedCode = '<?php ' . $content . (Strings::endsWith($content, ';') ? '' : ';');
/**
* @return Param[]
*/
private function parseStringToParameters(Expr $expr): array
{
$content = $this->stringify($expr);
$nodes = $this->parser->parse($wrappedCode);
if (count($nodes) === 1) {
if ($nodes[0] instanceof Expression) {
return [$nodes[0]->expr];
}
$content = '<?php $value = function(' . $content . ') {};';
return [$nodes[0]];
}
$nodes = $this->parser->parse($content);
// @todo implement
throw new ShouldNotHappenException();
return $nodes[0]->expr->expr->params;
}
/**
@ -163,15 +161,62 @@ CODE_SAMPLE
/** @var Variable[] $variableNodes */
$variableNodes = $this->betterNodeFinder->findInstanceOf($nodes, Variable::class);
foreach ($variableNodes as $i => $variableNode) {
if (! in_array($this->getName($variableNode), $paramNames, true)) {
$filteredVariables = [];
foreach ($variableNodes as $variableNode) {
// "$this" is allowed
if ($this->isName($variableNode, 'this')) {
continue;
}
unset($variableNodes[$i]);
if (in_array($this->getName($variableNode), $paramNames, true)) {
continue;
}
$filteredVariables[$this->getName($variableNode)] = $variableNode;
}
// re-index
return array_values($variableNodes);
return array_values($filteredVariables);
}
/**
* @param string|Node $content
*/
private function stringify($content): string
{
if (is_string($content)) {
return $content;
}
if ($content instanceof String_) {
return $content->value;
}
if ($content instanceof Encapsed) {
// remove "
$content = trim($this->print($content), '""');
// use \$ → $
$content = Strings::replace($content, '#\\\\\$#', '$');
// use \'{$...}\' → $...
return Strings::replace($content, '#\'{(\$.*?)}\'#', '$1');
}
if ($content instanceof Concat) {
return $this->stringify($content->left) . $this->stringify($content->right);
}
if ($content instanceof Variable || $content instanceof PropertyFetch) {
return $this->print($content);
}
throw new ShouldNotHappenException(get_class($content) . ' ' . __METHOD__);
}
private function createEval(Expr $node): Expression
{
$evalFuncCall = new FuncCall(new Name('eval'), [new Arg($node)]);
return new Expression($evalFuncCall);
}
}

View File

@ -9,7 +9,14 @@ final class CreateFunctionToAnonymousFunctionRectorTest extends AbstractRectorTe
{
public function test(): void
{
$this->doTestFiles([__DIR__ . '/Fixture/fixture.php.inc']);
$this->doTestFiles([
__DIR__ . '/Fixture/fixture.php.inc',
__DIR__ . '/Fixture/concat.php.inc',
__DIR__ . '/Fixture/reference.php.inc',
__DIR__ . '/Fixture/stackoverflow.php.inc',
__DIR__ . '/Fixture/drupal.php.inc',
__DIR__ . '/Fixture/php_net.php.inc',
]);
}
protected function getRectorClass(): string

View File

@ -0,0 +1,37 @@
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class ClassWithCreateFunctionWithConcat
{
public function run()
{
$value = 1;
$callback = create_function( '$m', 'return $m->meta_id == '.$value.';');
$callback = create_function('$a', 'return "<cas:proxy>$a</cas:proxy>";');
}
}
?>
-----
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class ClassWithCreateFunctionWithConcat
{
public function run()
{
$value = 1;
$callback = function ($m) use ($value) {
return $m->meta_id == $value;
};
$callback = function ($a) {
return "<cas:proxy>{$a}</cas:proxy>";
};
}
}
?>

View File

@ -0,0 +1,43 @@
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class Drupal
{
public function run()
{
$replace = create_function(
'$m',
'$r="\x00{$m[1]}ecursion_features";
return \'s:\'.strlen($r.$m[2]).\':"\'.$r.$m[2].\'";\';'
);
$replace = create_function(
'$matches',
'return $matches[1]?"-":"";'
);
}
}
?>
-----
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class Drupal
{
public function run()
{
$replace = function ($m) use ($r) {
$r = "\0{$m[1]}ecursion_features";
return 's:' . strlen($r . $m[2]) . ':"' . $r . $m[2] . '";';
};
$replace = function ($matches) {
return $matches[1] ? "-" : "";
};
}
}
?>

View File

@ -1,5 +1,7 @@
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class ClassWithCreateFunction
{
public function run()
@ -8,6 +10,22 @@ class ClassWithCreateFunction
$delimiter = '___';
$callback = create_function('$matches', "return '$delimiter' . strtolower(\$matches[1]);");
$callback = create_function( '$caps', "return '$delimiter';" );
add_action(
'widgets_init',
create_function(
'',
'return register_widget( "WordAds_Sidebar_Widget" );'
)
);
$all_ids = array_map( create_function( '$o', 'return $o->ID;' ), $posts);
$missing = create_function( '$m', 'return $m->type === \'post\';');
create_function('$caps', "return '$caps';");
}
}
@ -15,6 +33,8 @@ class ClassWithCreateFunction
-----
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class ClassWithCreateFunction
{
public function run()
@ -24,9 +44,32 @@ class ClassWithCreateFunction
};
$delimiter = '___';
$callback = function ($matches) use($delimiter) {
$callback = function ($matches) use ($delimiter) {
return $delimiter . strtolower($matches[1]);
};
$callback = function ($caps) use ($delimiter) {
return $delimiter;
};
add_action(
'widgets_init',
function () {
return register_widget("WordAds_Sidebar_Widget");
}
);
$all_ids = array_map( function ($o) {
return $o->ID;
}, $posts);
$missing = function ($m) {
return $m->type === 'post';
};
function ($caps) {
return $caps;
};
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class PhpNet
{
public function run()
{
create_function('$x,$y', 'return "some trig: ".(sin($x) + $x*cos($y));');
create_function('$x,$y', 'return "a hypotenuse: ".sqrt($x*$x + $y*$y);');
create_function('$a,$b', 'if ($a >=0) {return "b*a^2 = ".$b*sqrt($a);} else {return false;}');
create_function('$a,$b', "return \"min(b^2+a, a^2,b) = \".min(\$a*\$a+\$b,\$b*\$b+\$a);");
create_function('$a,$b', 'if ($a > 0 && $b != 0) {return "ln(a)/b = ".log($a)/$b; } else { return false; }');
create_function('$b,$a', 'if (strncmp($a, $b, 3) == 0) return "** \"$a\" '.
'and \"$b\"** Look the same to me! (looking at the first 3 chars)";');
create_function('$a,$b', '; return "CRCs: " . crc32($a) . ", ".crc32($b);');
create_function('$a,$b', '; return "similar(a,b) = " . similar_text($a, $b, &$p) . "($p%)";');
create_function('&$v,$k', '$v = $v . "mango";');
}
}
?>
-----
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class PhpNet
{
public function run()
{
function ($x, $y) {
return "some trig: " . (sin($x) + $x * cos($y));
};
function ($x, $y) {
return "a hypotenuse: " . sqrt($x * $x + $y * $y);
};
function ($a, $b) {
if ($a >= 0) {
return "b*a^2 = " . $b * sqrt($a);
} else {
return false;
}
};
function ($a, $b) {
return "min(b^2+a, a^2,b) = " . min($a * $a + $b, $b * $b + $a);
};
function ($a, $b) {
if ($a > 0 && $b != 0) {
return "ln(a)/b = " . log($a) / $b;
} else {
return false;
}
};
function ($b, $a) {
if (strncmp($a, $b, 3) == 0) {
return "** \"{$a}\" and \"{$b}\"** Look the same to me! (looking at the first 3 chars)";
}
};
function ($a, $b) {
return "CRCs: " . crc32($a) . ", " . crc32($b);
};
function ($a, $b) use ($p) {
return "similar(a,b) = " . similar_text($a, $b, &$p) . "({$p}%)";
};
function (&$v, $k) {
$v = $v . "mango";
};
}
}
?>

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class ClassWithCreateFunctionWithReference
{
public function run()
{
$value = 1;
$callback = create_function('&$attributes', $this->code);
}
}
?>
-----
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class ClassWithCreateFunctionWithReference
{
public function run()
{
$value = 1;
$callback = function (&$attributes) {
eval($this->code);
};
}
}
?>

View File

@ -0,0 +1,45 @@
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class ClassWithCreateFunctionWithStackoverflow
{
public function run()
{
$func = create_function('$atts, $content = null','return "<div class=\"' . $class_list . '\">" . do_shortcode($content) . "</div>";' );
add_shortcode($shortcode, $func);
$this->translation_plural['callable'] = create_function('$n', $this->translation_plural['function']);
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
}
}
?>
-----
<?php
namespace Rector\Php\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\Fixture;
class ClassWithCreateFunctionWithStackoverflow
{
public function run()
{
$func = function ($atts, $content = null) use ($class_list) {
return "<div class=\"{$class_list}\">" . do_shortcode($content) . "</div>";
};
add_shortcode($shortcode, $func);
$this->translation_plural['callable'] = function ($n) {
eval($this->translation_plural['function']);
};
$newfunc = function ($a, $b) {
return "ln({$a}) + ln({$b}) = " . log($a * $b);
};
}
}
?>

View File

@ -7,6 +7,7 @@ use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\BinaryOp\Coalesce;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
@ -58,6 +59,14 @@ final class BetterStandardPrinter extends Standard
return '\'' . preg_replace("/'|\\\\(?=[\\\\']|$)/", '\\\\$0', $string) . '\'';
}
/**
* Add space after "use ("
*/
protected function pExpr_Closure(Closure $node): string
{
return Strings::replace(parent::pExpr_Closure($node), '#( use)\(#', '$1 (');
}
/**
* Do not add "()" on Expressions
* @see https://github.com/rectorphp/rector/pull/401#discussion_r181487199

View File

@ -93,7 +93,6 @@ abstract class AbstractRector extends NodeVisitorAbstract implements PhpRectorIn
*/
public function afterTraverse(array $nodes): array
{
// @todo foreach, array autowire on Required in CommanderInterface[]
if ($this->nodeAddingCommander->isActive()) {
$nodes = $this->nodeAddingCommander->traverseNodes($nodes);
}