Skip to content
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
9 changes: 9 additions & 0 deletions rules/php55/config/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
_defaults:
public: true
autowire: true

Rector\Php55\:
resource: '../src'
exclude:
- '../src/Rector/**/*Rector.php'
77 changes: 77 additions & 0 deletions rules/php55/src/NodeFactory/AnonymousFunctionNodeFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Rector\Php55\NodeFactory;

use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Parser;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\PhpParser\NodeTraverser\CallableNodeTraverser;

final class AnonymousFunctionNodeFactory
{
/**
* @var Parser
*/
private $parser;

/**
* @var CallableNodeTraverser
*/
private $callableNodeTraverser;

public function __construct(Parser $parser, CallableNodeTraverser $callableNodeTraverser)
{
$this->parser = $parser;
$this->callableNodeTraverser = $callableNodeTraverser;
}

public function createAnonymousFunctionFromString(Expr $expr): ?Closure
{
if (! $expr instanceof String_) {
// not supported yet
throw new ShouldNotHappenException();
}

$phpCode = '<?php ' . $expr->value . ';';
$contentNodes = $this->parser->parse($phpCode);

$anonymousFunction = new Closure();
if (! $contentNodes[0] instanceof Expression) {
return null;
}

$stmt = $contentNodes[0]->expr;

$this->callableNodeTraverser->traverseNodesWithCallable($stmt, function (Node $node): Node {
if (! $node instanceof String_) {
return $node;
}

$match = Strings::match($node->value, '#(\\$|\\\\|\\x0)(?<number>\d+)#');
if (! $match) {
return $node;
}

$matchesVariable = new Variable('matches');

return new ArrayDimFetch($matchesVariable, new LNumber((int) $match['number']));
});

$anonymousFunction->stmts[] = new Return_($stmt);
$anonymousFunction->params[] = new Param(new Variable('matches'));

return $anonymousFunction;
}
}
89 changes: 23 additions & 66 deletions rules/php55/src/Rector/FuncCall/PregReplaceEModifierRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,14 @@

namespace Rector\Php55\Rector\FuncCall;

use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Parser;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\Php55\NodeFactory\AnonymousFunctionNodeFactory;
use Rector\Php55\RegexMatcher;

/**
* @see https://wiki.php.net/rfc/remove_preg_replace_eval_modifier
Expand All @@ -32,13 +22,21 @@
final class PregReplaceEModifierRector extends AbstractRector
{
/**
* @var Parser
* @var RegexMatcher
*/
private $parser;
private $regexMatcher;

public function __construct(Parser $parser)
{
$this->parser = $parser;
/**
* @var AnonymousFunctionNodeFactory
*/
private $anonymousFunctionNodeFactory;

public function __construct(
RegexMatcher $regexMatcher,
AnonymousFunctionNodeFactory $anonymousFunctionNodeFactory
) {
$this->regexMatcher = $regexMatcher;
$this->anonymousFunctionNodeFactory = $anonymousFunctionNodeFactory;
}

public function getDefinition(): RectorDefinition
Expand Down Expand Up @@ -87,65 +85,24 @@ public function refactor(Node $node): ?Node
return null;
}

$pattern = $this->getValue($node->args[0]->value);
$delimiter = $pattern[0];

/** @var string $modifiers */
$modifiers = Strings::after($pattern, $delimiter, -1);
if (! Strings::contains($modifiers, 'e')) {
$firstArgumentValue = $node->args[0]->value;
$patternWithoutE = $this->regexMatcher->resolvePatternExpressionWithoutEIfFound($firstArgumentValue);
if ($patternWithoutE === null) {
return null;
}

$modifiersWithoutE = Strings::replace($modifiers, '#e#');
$patternWithoutE = Strings::before($pattern, $delimiter, -1) . $delimiter . $modifiersWithoutE;

$anonymousFunction = $this->createAnonymousFunctionFromString($node->args[1]->value);
$secondArgumentValue = $node->args[1]->value;
$anonymousFunction = $this->anonymousFunctionNodeFactory->createAnonymousFunctionFromString(
$secondArgumentValue
);
if ($anonymousFunction === null) {
return null;
}

$node->name = new Name('preg_replace_callback');
$node->args[0]->value = new String_($patternWithoutE);
$node->args[0]->value = $patternWithoutE;
$node->args[1]->value = $anonymousFunction;

return $node;
}

private function createAnonymousFunctionFromString(Expr $expr): ?Closure
{
if (! $expr instanceof String_) {
// not supported yet
throw new ShouldNotHappenException();
}

$phpCode = '<?php ' . $expr->value . ';';
$contentNodes = $this->parser->parse($phpCode);

$anonymousFunction = new Closure();
if (! $contentNodes[0] instanceof Expression) {
return null;
}

$stmt = $contentNodes[0]->expr;

$this->traverseNodesWithCallable($stmt, function (Node $node): Node {
if (! $node instanceof String_) {
return $node;
}

$match = Strings::match($node->value, '#(\\$|\\\\|\\x0)(?<number>\d+)#');
if (! $match) {
return $node;
}

$matchesVariable = new Variable('matches');

return new ArrayDimFetch($matchesVariable, new LNumber((int) $match['number']));
});

$anonymousFunction->stmts[] = new Return_($stmt);
$anonymousFunction->params[] = new Param(new Variable('matches'));

return $anonymousFunction;
}
}
85 changes: 85 additions & 0 deletions rules/php55/src/RegexMatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace Rector\Php55;

use Nette\Utils\Strings;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Scalar\String_;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\PhpParser\Node\Value\ValueResolver;

final class RegexMatcher
{
/**
* @var ValueResolver
*/
private $valueResolver;

public function __construct(ValueResolver $valueResolver)
{
$this->valueResolver = $valueResolver;
}

public function resolvePatternExpressionWithoutEIfFound(Expr $expr): ?Expr
{
if ($expr instanceof String_) {
$pattern = $this->valueResolver->getValue($expr);

if (! is_string($pattern)) {
return null;
}

$delimiter = $pattern[0];
if (! is_string($delimiter)) {
throw new ShouldNotHappenException();
}

/** @var string $modifiers */
$modifiers = Strings::after($pattern, $delimiter, -1);
if (! Strings::contains($modifiers, 'e')) {
return null;
}

$patternWithoutE = $this->createPatternWithoutE($pattern, $delimiter, $modifiers);
return new String_($patternWithoutE);
}

if ($expr instanceof Concat) {
return $this->matchConcat($expr);
}

return null;
}

private function createPatternWithoutE(string $pattern, string $delimiter, string $modifiers): string
{
$modifiersWithoutE = Strings::replace($modifiers, '#e#');

return Strings::before($pattern, $delimiter, -1) . $delimiter . $modifiersWithoutE;
}

private function matchConcat(Expr $expr): ?Expr
{
$lastItem = $expr->right;
if (! $lastItem instanceof String_) {
return null;
}

$matches = Strings::match($lastItem->value, '#(?<modifiers>\w+)$#');
if (! isset($matches['modifiers'])) {
return null;
}

if (! Strings::contains($matches['modifiers'], 'e')) {
return null;
}

// replace last "e" in the code
$lastItem->value = Strings::replace($lastItem->value, '#(\w+)?e(\w+)?$#', '$1$2');

return $expr;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Rector\Php55\Tests\Rector\FuncCall\PregReplaceEModifierRector\Fixture;

class PregQuoted
{
public function run($subject)
{
preg_replace('/('.preg_quote("a", '/').')/e', 'strtoupper("\1")', $subject);
}
}

?>
-----
<?php

namespace Rector\Php55\Tests\Rector\FuncCall\PregReplaceEModifierRector\Fixture;

class PregQuoted
{
public function run($subject)
{
preg_replace_callback('/('.preg_quote("a", '/').')/', function ($matches) {
return strtoupper("\1");
}, $subject);
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Rector\Php55\Tests\Rector\FuncCall\PregReplaceEModifierRector\Fixture;

class RemoveOnlyEndE
{
public function run()
{
$comment = preg_replace('~e~e', '"ahoj"', $comment);
}
}

?>
-----
<?php

namespace Rector\Php55\Tests\Rector\FuncCall\PregReplaceEModifierRector\Fixture;

class RemoveOnlyEndE
{
public function run()
{
$comment = preg_replace_callback('~e~', function ($matches) {
return "ahoj";
}, $comment);
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Rector\Php55\Tests\Rector\FuncCall\PregReplaceEModifierRector\Fixture;

class SkipNonString
{
public function run($contents)
{
$contents = preg_replace(['/\s+$/Sm', '/\n+/S'], "\n", $contents);
}
}
2 changes: 1 addition & 1 deletion src/Rector/AbstractRector/ValueResolverTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ trait ValueResolverTrait
/**
* @required
*/
public function setValueResolver(ValueResolver $valueResolver): void
public function autowireValueResolverTrait(ValueResolver $valueResolver): void
{
$this->valueResolver = $valueResolver;
}
Expand Down