fix CallableThisArrayToAnonymousFunctionRector for invalid array items

This commit is contained in:
Tomas Votruba 2019-05-07 15:49:21 +02:00
parent de331920c8
commit adc50652a0
5 changed files with 240 additions and 16 deletions

View File

@ -2,6 +2,8 @@
namespace Rector\CodeQuality\Rector\Array_;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
@ -93,19 +95,21 @@ CODE_SAMPLE
*/
public function refactor(Node $node): ?Node
{
if (count($node->items) !== 2) {
return null;
}
// is callable?
// can be totally empty, e.g [, $value]
if ($node->items[0] === null) {
if ($this->shouldSkipArray($node)) {
return null;
}
$objectVariable = $node->items[0]->value;
if (! $objectVariable instanceof Variable && ! $objectVariable instanceof PropertyFetch) {
return null;
}
$classMethod = $this->matchCallableMethod($objectVariable, $node->items[1]->value);
$methodName = $node->items[1]->value;
if (! $methodName instanceof String_) {
return null;
}
$classMethod = $this->matchCallableMethod($objectVariable, $methodName);
if ($classMethod === null) {
return null;
}
@ -145,7 +149,10 @@ CODE_SAMPLE
return $args;
}
private function matchCallableMethod(Expr $objectExpr, Expr $methodExpr): ?ClassMethod
/**
* @param Variable|PropertyFetch $objectExpr
*/
private function matchCallableMethod(Expr $objectExpr, String_ $methodExpr): ?ClassMethod
{
$methodName = $this->getValue($methodExpr);
@ -173,4 +180,15 @@ CODE_SAMPLE
return null;
}
private function shouldSkipArray(Array_ $array): bool
{
// callback is exactly "[$two, 'items']"
if (count($array->items) !== 2) {
return true;
}
// can be totally empty in case of "[, $value]"
return $array->items[0] === null;
}
}

View File

@ -13,7 +13,9 @@ final class CallableThisArrayToAnonymousFunctionRectorTest extends AbstractRecto
__DIR__ . '/Fixture/fixture.php.inc',
__DIR__ . '/Fixture/skip.php.inc',
__DIR__ . '/Fixture/another_class.php.inc',
__DIR__ . '/Fixture/skip_too.php.inc',
__DIR__ . '/Fixture/skip_another_class.php.inc',
__DIR__ . '/Fixture/skip_empty_first_array.php.inc',
__DIR__ . '/Fixture/skip_as_well.php.inc',
]);
}

View File

@ -0,0 +1,207 @@
<?php declare(strict_types=1);
namespace Rector\CodeQuality\Tests\Rector\Array_\CallableThisArrayToAnonymousFunctionRector\Fixture;
use PhpParser\Node;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Property;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\ConfiguredCodeSample;
use Rector\RectorDefinition\RectorDefinition;
final class PseudoNamespaceToNamespaceRector
{
/**
* @var string|null
*/
private $newNamespace;
/**
* @var string[][]|null[]
*/
private $namespacePrefixesWithExcludedClasses = [];
/**
* @var ClassManipulator
*/
private $classManipulator;
/**
* @var DocBlockManipulator
*/
private $docBlockManipulator;
/**
* @param string[][]|null[] $namespacePrefixesWithExcludedClasses
*/
public function __construct(
ClassManipulator $classManipulator,
DocBlockManipulator $docBlockManipulator,
array $namespacePrefixesWithExcludedClasses = []
) {
$this->classManipulator = $classManipulator;
$this->namespacePrefixesWithExcludedClasses = $namespacePrefixesWithExcludedClasses;
$this->docBlockManipulator = $docBlockManipulator;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Replaces defined Pseudo_Namespaces by Namespace\Ones.', [
new ConfiguredCodeSample(
'$someService = new Some_Object;',
'$someService = new Some\Object;',
[
['Some_' => []],
]
),
new ConfiguredCodeSample(
<<<'CODE_SAMPLE'
/** @var Some_Object $someService */
$someService = new Some_Object;
$someClassToKeep = new Some_Class_To_Keep;
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
/** @var Some\Object $someService */
$someService = new Some\Object;
$someClassToKeep = new Some_Class_To_Keep;
CODE_SAMPLE
,
[
['Some_' => ['Some_Class_To_Keep']],
]
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
// property, method
return [Name::class, Identifier::class, Property::class, FunctionLike::class, Expression::class];
}
/**
* @param Name|Identifier|Property|FunctionLike $node
*/
public function refactor(Node $node): ?Node
{
// replace on @var/@param/@return/@throws
foreach ($this->namespacePrefixesWithExcludedClasses as $namespacePrefix => $excludedClasses) {
$this->docBlockManipulator->changeUnderscoreType($node, $namespacePrefix, $excludedClasses);
}
if ($node instanceof Name || $node instanceof Identifier) {
return $this->processNameOrIdentifier($node);
}
return null;
}
/**
* @param Stmt[] $nodes
* @return Node[]
*/
public function afterTraverse(array $nodes): array
{
if ($this->newNamespace === null) {
return $nodes;
}
$namespaceNode = new Namespace_(new Name($this->newNamespace));
foreach ($nodes as $key => $node) {
if ($node instanceof Class_) {
$nodes = $this->classManipulator->insertBeforeAndFollowWithNewline($nodes, $namespaceNode, $key);
break;
}
}
$this->newNamespace = null;
return $nodes;
}
private function processName(Name $name): Name
{
$nodeName = $this->getName($name);
if ($nodeName !== null) {
$name->parts = explode('_', $nodeName);
}
return $name;
}
private function processIdentifier(Identifier $identifier): ?Identifier
{
$parentNode = $identifier->getAttribute(AttributeKey::PARENT_NODE);
if (! $parentNode instanceof Class_) {
return null;
}
$name = $this->getName($identifier);
if ($name === null) {
return null;
}
$newNameParts = explode('_', $name);
$lastNewNamePart = $newNameParts[count($newNameParts) - 1];
$namespaceParts = $newNameParts;
array_pop($namespaceParts);
$newNamespace = implode('\\', $namespaceParts);
if ($this->newNamespace !== null && $this->newNamespace !== $newNamespace) {
throw new ShouldNotHappenException('There cannot be 2 different namespaces in one file');
}
$this->newNamespace = $newNamespace;
$identifier->name = $lastNewNamePart;
return $identifier;
}
/**
* @param Name|Identifier $node
* @return Name|Identifier
*/
private function processNameOrIdentifier(Node $node): ?Node
{
// no name → skip
if ($node->toString() === '') {
return null;
}
foreach ($this->namespacePrefixesWithExcludedClasses as $namespacePrefix => $excludedClasses) {
if (! $this->isName($node, $namespacePrefix . '*')) {
continue;
}
if (is_array($excludedClasses) && $this->isNames($node, $excludedClasses)) {
return null;
}
if ($node instanceof Name) {
return $this->processName($node);
}
return $this->processIdentifier($node);
}
return null;
}
}

View File

@ -2,13 +2,10 @@
namespace Rector\CodeQuality\Tests\Rector\Array_\CallableThisArrayToAnonymousFunctionRector\Fixture;
use PhpParser\Node;
use PhpParser\Node\Stmt\Foreach_;
final class ForeachToInArrayRector
{
public function refactor(Node $node)
public function refactor($node)
{
[, $comparedNode] = $matchedNodes;
[, $comparedNode] = $node;
}
}

View File

@ -334,7 +334,7 @@ CODE_SAMPLE
private function isCurrentNamespace(string $namespaceName, string $newUseStatement): bool
{
$afterCurrentNamespace = Strings::after($newUseStatement, $namespaceName . '\\');
if ($afterCurrentNamespace === false) {
if (! $afterCurrentNamespace) {
return false;
}