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
2 changes: 2 additions & 0 deletions config/set/phpunit/phpunit-injector.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
services:
Rector\PHPUnit\Rector\Class_\SelfContainerGetMethodCallFromTestToInjectPropertyRector: ~
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
use Rector\ValueObject\PhpVersionFeature;

/**
* @see https://3v4l.org/GL6II
Expand Down Expand Up @@ -210,7 +211,7 @@ private function createNewProperties(array $fetchedLocalPropertyNameToTypes, arr
$propertyBuilder->makePublic();
$property = $propertyBuilder->getNode();

if ($this->isAtLeastPhpVersion('7.4')) {
if ($this->isAtLeastPhpVersion(PhpVersionFeature::TYPED_PROPERTIES)) {
$phpStanNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($propertyType);
if ($phpStanNode !== null) {
$property->type = $phpStanNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Iterator;
use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Rector\ValueObject\PhpVersionFeature;

final class CompleteDynamicPropertiesRectorTest extends AbstractRectorTestCase
{
Expand All @@ -30,7 +31,6 @@ protected function getRectorClass(): string

protected function getPhpVersion(): string
{
// prevents union types
return '7.4';
return PhpVersionFeature::BEFORE_UNION_TYPES;
}
}
167 changes: 167 additions & 0 deletions packages/PHPUnit/src/Manipulator/OnContainerGetCallManipulator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php

declare(strict_types=1);

namespace Rector\PHPUnit\Manipulator;

use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Class_;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpParser\Node\Commander\NodeRemovingCommander;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\PhpParser\Node\Value\ValueResolver;
use Rector\PhpParser\NodeTraverser\CallableNodeTraverser;
use Rector\SymfonyPHPUnit\Naming\ServiceNaming;
use Rector\SymfonyPHPUnit\Node\KernelTestCaseNodeAnalyzer;

final class OnContainerGetCallManipulator
{
/**
* @var NameResolver
*/
private $nameResolver;

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

/**
* @var ServiceNaming
*/
private $serviceNaming;

/**
* @var NodeRemovingCommander
*/
private $nodeRemovingCommander;

/**
* @var KernelTestCaseNodeAnalyzer
*/
private $kernelTestCaseNodeAnalyzer;

/**
* @var ValueResolver
*/
private $valueResolver;

public function __construct(
NameResolver $nameResolver,
CallableNodeTraverser $callableNodeTraverser,
ServiceNaming $serviceNaming,
NodeRemovingCommander $nodeRemovingCommander,
KernelTestCaseNodeAnalyzer $kernelTestCaseNodeAnalyzer,
ValueResolver $valueResolver
) {
$this->nameResolver = $nameResolver;
$this->callableNodeTraverser = $callableNodeTraverser;
$this->serviceNaming = $serviceNaming;
$this->nodeRemovingCommander = $nodeRemovingCommander;
$this->kernelTestCaseNodeAnalyzer = $kernelTestCaseNodeAnalyzer;
$this->valueResolver = $valueResolver;
}

/**
* E.g. $someService ↓
* $this->someService
*
* @param string[][] $formerVariablesByMethods
*/
public function replaceFormerVariablesWithPropertyFetch(Class_ $class, array $formerVariablesByMethods): void
{
$this->callableNodeTraverser->traverseNodesWithCallable($class->stmts, function (Node $node) use (
$formerVariablesByMethods
): ?PropertyFetch {
if (! $node instanceof Variable) {
return null;
}

$variableName = $this->nameResolver->getName($node);
if ($variableName === null) {
return null;
}

/** @var string $methodName */
$methodName = $node->getAttribute(AttributeKey::METHOD_NAME);
if (! isset($formerVariablesByMethods[$methodName][$variableName])) {
return null;
}

$serviceType = $formerVariablesByMethods[$methodName][$variableName];
$propertyName = $this->serviceNaming->resolvePropertyNameFromServiceType($serviceType);

return new PropertyFetch(new Variable('this'), $propertyName);
});
}

/**
* @return string[][]
*/
public function removeAndCollectFormerAssignedVariables(Class_ $class, bool $skipSetUpMethod = true): array
{
$formerVariablesByMethods = [];

$this->callableNodeTraverser->traverseNodesWithCallable($class->stmts, function (Node $node) use (
&$formerVariablesByMethods,
$skipSetUpMethod
): ?PropertyFetch {
if (! $node instanceof MethodCall) {
return null;
}

if ($skipSetUpMethod) {
if ($this->kernelTestCaseNodeAnalyzer->isSetUpOrEmptyMethod($node)) {
return null;
}
}

if (! $this->kernelTestCaseNodeAnalyzer->isOnContainerGetMethodCall($node)) {
return null;
}

$type = $this->valueResolver->getValue($node->args[0]->value);
if ($type === null) {
return null;
}

$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);

if ($parentNode instanceof Assign) {
$this->processAssign($node, $parentNode, $type, $formerVariablesByMethods);
return null;
}

$propertyName = $this->serviceNaming->resolvePropertyNameFromServiceType($type);

return new PropertyFetch(new Variable('this'), $propertyName);
});

return $formerVariablesByMethods;
}

/**
* @param string[][] $formerVariablesByMethods
*/
private function processAssign(
MethodCall $methodCall,
Assign $assign,
string $type,
array &$formerVariablesByMethods
): void {
$variableName = $this->nameResolver->getName($assign->var);
if ($variableName === null) {
return;
}

/** @var string $methodName */
$methodName = $methodCall->getAttribute(AttributeKey::METHOD_NAME);
$formerVariablesByMethods[$methodName][$variableName] = $type;

$this->nodeRemovingCommander->addNode($assign);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php

declare(strict_types=1);

namespace Rector\PHPUnit\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\AttributeAwarePhpDocTagNode;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\PHPUnit\Manipulator\OnContainerGetCallManipulator;
use Rector\Rector\AbstractPHPUnitRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
use Rector\SymfonyPHPUnit\Node\KernelTestCaseNodeFactory;
use Rector\SymfonyPHPUnit\Rector\Class_\SelfContainerGetMethodCallFromTestToSetUpMethodRector;
use Rector\SymfonyPHPUnit\SelfContainerMethodCallCollector;

/**
* Inspiration
* @see SelfContainerGetMethodCallFromTestToSetUpMethodRector
*
* @see https://github.com/shopsys/shopsys/pull/1392
* @see https://github.com/jakzal/phpunit-injector
*
* @see \Rector\PHPUnit\Tests\Rector\Class_\SelfContainerGetMethodCallFromTestToInjectPropertyRector\SelfContainerGetMethodCallFromTestToInjectPropertyRectorTest
*/
final class SelfContainerGetMethodCallFromTestToInjectPropertyRector extends AbstractPHPUnitRector
{
/**
* @var SelfContainerMethodCallCollector
*/
private $selfContainerMethodCallCollector;

/**
* @var KernelTestCaseNodeFactory
*/
private $kernelTestCaseNodeFactory;

/**
* @var OnContainerGetCallManipulator
*/
private $onContainerGetCallManipulator;

/**
* @var ClassManipulator
*/
private $classManipulator;

public function __construct(
SelfContainerMethodCallCollector $selfContainerMethodCallCollector,
KernelTestCaseNodeFactory $kernelTestCaseNodeFactory,
OnContainerGetCallManipulator $onContainerGetCallManipulator,
ClassManipulator $classManipulator
) {
$this->selfContainerMethodCallCollector = $selfContainerMethodCallCollector;
$this->kernelTestCaseNodeFactory = $kernelTestCaseNodeFactory;
$this->onContainerGetCallManipulator = $onContainerGetCallManipulator;
$this->classManipulator = $classManipulator;
}

public function getDefinition(): RectorDefinition
{
return new RectorDefinition(
'Change $container->get() calls in PHPUnit to @inject properties autowired by jakzal/phpunit-injector',
[
new CodeSample(
<<<'PHP'
use PHPUnit\Framework\TestCase;
class SomeClassTest extends TestCase {
public function test()
{
$someService = $this->getContainer()->get(SomeService::class);
}
}
class SomeService { }
PHP
,
<<<'PHP'
use PHPUnit\Framework\TestCase;
class SomeClassTest extends TestCase {
/**
* @var SomeService
* @inject
*/
private $someService;
public function test()
{
$someService = $this->someService;
}
}
class SomeService { }
PHP

),
]
);
}

/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->isInTestClass($node)) {
return null;
}

// 1. find self::$container->get(x)
$serviceTypes = $this->selfContainerMethodCallCollector->collectContainerGetServiceTypes($node, false);
if (count($serviceTypes) === 0) {
return null;
}

// 2,5 - add @inject to existing properties of that type, to prevent re-adding them
foreach ($serviceTypes as $key => $serviceType) {
$existingProperty = $this->classManipulator->findPropertyByType($node, $serviceType);
if ($existingProperty !== null) {
$this->addInjectAnnotationToProperty($existingProperty);
unset($serviceTypes[$key]);
}
}

// 2. create private properties with this types
$privateProperties = $this->kernelTestCaseNodeFactory->createPrivatePropertiesFromTypes($node, $serviceTypes);
$this->addInjectAnnotationToProperties($privateProperties);
$node->stmts = array_merge($privateProperties, $node->stmts);

// 3. remove old in-method $property assigns
$formerVariablesByMethods = $this->onContainerGetCallManipulator->removeAndCollectFormerAssignedVariables(
$node,
false
);

// 4. replace former variables by $this->someProperty
$this->onContainerGetCallManipulator->replaceFormerVariablesWithPropertyFetch($node, $formerVariablesByMethods);

return $node;
}

/**
* @param Property[] $properties
*/
private function addInjectAnnotationToProperties(array $properties): void
{
foreach ($properties as $privateProperty) {
$this->addInjectAnnotationToProperty($privateProperty);
}
}

private function addInjectAnnotationToProperty(Property $privateProperty): void
{
/** @var PhpDocInfo $phpDocInfo */
$phpDocInfo = $this->getPhpDocInfo($privateProperty);
$phpDocNode = $phpDocInfo->getPhpDocNode();
$phpDocNode->children[] = new AttributeAwarePhpDocTagNode('@inject', new GenericTagValueNode(''));

$this->docBlockManipulator->updateNodeWithPhpDocInfo($privateProperty, $phpDocInfo);
}
}
Loading