diff --git a/config/sets/phpunit-code-quality.php b/config/sets/phpunit-code-quality.php index 6b2c801b..73ed3bd9 100644 --- a/config/sets/phpunit-code-quality.php +++ b/config/sets/phpunit-code-quality.php @@ -7,6 +7,7 @@ use Rector\PHPUnit\CodeQuality\Rector\Class_\AddParamTypeFromDependsRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\AddReturnTypeToDependedRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\ConstructClassMethodToSetUpTestCaseRector; +use Rector\PHPUnit\CodeQuality\Rector\Class_\InlineStubPropertyToCreateStubMethodCallRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\NarrowUnusedSetUpDefinedPropertyRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\SingleMockPropertyTypeRector; @@ -132,6 +133,7 @@ CreateStubOverCreateMockArgRector::class, ExpressionCreateMockToCreateStubRector::class, PropertyCreateMockToCreateStubRector::class, + InlineStubPropertyToCreateStubMethodCallRector::class, // @test first, enable later // \Rector\PHPUnit\CodeQuality\Rector\Expression\ConfiguredMockEntityToSetterObjectRector::class, diff --git a/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/inline_stub_only_property.php.inc b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/inline_stub_only_property.php.inc new file mode 100644 index 00000000..8a336607 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/inline_stub_only_property.php.inc @@ -0,0 +1,46 @@ +someStub = $this->createStub(NotRelevantClass::class); + } + + public function testAnother() + { + $anotherObject = new NotRelevantClass($this->someStub); + } +} + +?> +----- +createStub(\Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\InlineStubPropertyToCreateStubMethodCallRector\Source\NotRelevantClass::class)); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/skip_if_used.php.inc b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/skip_if_used.php.inc new file mode 100644 index 00000000..80445637 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Fixture/skip_if_used.php.inc @@ -0,0 +1,24 @@ +someStub = $this->createStub(NotRelevantClass::class); + } + + public function testAnother() + { + $someCall = $this->someStub->method('some_method'); + + $anotherObject = new NotRelevantClass($this->someStub); + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/InlineStubPropertyToCreateStubMethodCallRectorTest.php b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/InlineStubPropertyToCreateStubMethodCallRectorTest.php new file mode 100644 index 00000000..a206e25a --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/InlineStubPropertyToCreateStubMethodCallRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Source/NotRelevantClass.php b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Source/NotRelevantClass.php new file mode 100644 index 00000000..a880f2d2 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector/Source/NotRelevantClass.php @@ -0,0 +1,12 @@ +withRules([InlineStubPropertyToCreateStubMethodCallRector::class]); diff --git a/rules/CodeQuality/NodeAnalyser/StubPropertyResolver.php b/rules/CodeQuality/NodeAnalyser/StubPropertyResolver.php new file mode 100644 index 00000000..bd2d17a6 --- /dev/null +++ b/rules/CodeQuality/NodeAnalyser/StubPropertyResolver.php @@ -0,0 +1,69 @@ + + */ + public function resolveFromClassMethod(ClassMethod $classMethod): array + { + $propertyNamesToStubClasses = []; + + foreach ((array) $classMethod->stmts as $stmt) { + if (! $stmt instanceof Expression) { + continue; + } + + if (! $stmt->expr instanceof Assign) { + continue; + } + + $assign = $stmt->expr; + + if (! $assign->var instanceof PropertyFetch) { + continue; + } + + if (! $assign->expr instanceof MethodCall) { + continue; + } + + $methodCall = $assign->expr; + if (! $this->nodeNameResolver->isName($methodCall->name, 'createStub')) { + continue; + } + + $propertyFetch = $assign->var; + $propertyName = $this->nodeNameResolver->getName($propertyFetch->name); + + if (! is_string($propertyName)) { + continue; + } + + $firstArg = $methodCall->getArgs()[0]; + $stubbedClassName = $this->valueResolver->getValue($firstArg->value); + + $propertyNamesToStubClasses[$propertyName] = $stubbedClassName; + } + + return $propertyNamesToStubClasses; + } +} diff --git a/rules/CodeQuality/NodeFinder/PropertyFetchUsageFinder.php b/rules/CodeQuality/NodeFinder/PropertyFetchUsageFinder.php new file mode 100644 index 00000000..fe9d0360 --- /dev/null +++ b/rules/CodeQuality/NodeFinder/PropertyFetchUsageFinder.php @@ -0,0 +1,51 @@ +betterNodeFinder->findInstancesOfScoped($class->getMethods(), New_::class); + + $propertyFetchesInNewArgs = []; + + foreach ($news as $new) { + if ($new->isFirstClassCallable()) { + continue; + } + + foreach ($new->getArgs() as $arg) { + if (! $arg->value instanceof PropertyFetch) { + continue; + } + + if (! $this->nodeNameResolver->isName($arg->value->name, $propertyName)) { + continue; + } + + $propertyFetchesInNewArgs[] = $arg->value; + } + } + + return $propertyFetchesInNewArgs; + } +} diff --git a/rules/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector.php b/rules/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector.php new file mode 100644 index 00000000..35114103 --- /dev/null +++ b/rules/CodeQuality/Rector/Class_/InlineStubPropertyToCreateStubMethodCallRector.php @@ -0,0 +1,218 @@ +someStub = $this->createStub(SomeClass::class); + } + + public function testAnother() + { + $anotherObject = new AnotherObject($this->someStub); + } +} +CODE_SAMPLE + + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + public function testAnother() + { + $anotherObject = new AnotherObject($this->createStub(SomeStub::class)); + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + $setUpClassMethod = $node->getMethod(MethodName::SET_UP); + if (! $setUpClassMethod instanceof ClassMethod) { + return null; + } + + $propertyNamesToStubClasses = $this->stubPropertyResolver->resolveFromClassMethod($setUpClassMethod); + if ($propertyNamesToStubClasses === []) { + return null; + } + + $hasChanged = false; + foreach ($node->stmts as $key => $stmt) { + if (! $stmt instanceof Property) { + continue; + } + + $soleProperty = $this->matchSoleStubPropertyItem($stmt); + if (! $soleProperty instanceof PropertyItem) { + continue; + } + + $propertyName = $this->getName($soleProperty); + if (! isset($propertyNamesToStubClasses[$propertyName])) { + continue; + } + + $currentPropertyFetchesInNewArgs = $this->propertyFetchUsageFinder->findInNew($node, $propertyName); + + // are there more uses than simple passing to a new instance? + $totalPropertyFetches = $this->propertyFetchFinder->findLocalPropertyFetchesByName($node, $propertyName); + if ((count($totalPropertyFetches) - 1) !== count($currentPropertyFetchesInNewArgs)) { + continue; + } + + $hasChanged = true; + + // 1. remove property + unset($node->stmts[$key]); + + // 2. remove property assign in setUp() + $this->removeSetupPropertyFetchByPropertyName($setUpClassMethod, $propertyName); + + // 3. replace property fetch calls, with createStub() + $stubClassName = $propertyNamesToStubClasses[$propertyName]; + $this->traverseNodesWithCallable($node->getMethods(), function (Node $node) use ( + $propertyName, + $stubClassName + ): ?MethodCall { + if (! $node instanceof PropertyFetch) { + return null; + } + + if (! $this->isName($node->name, $propertyName)) { + return null; + } + + $classConstFetch = new ClassConstFetch(new FullyQualified($stubClassName), 'class'); + + return new MethodCall(new Variable('this'), new Identifier('createStub'), [ + new Arg($classConstFetch), + ]); + }); + } + + if ($hasChanged === false) { + return null; + } + + return $node; + } + + private function removeSetupPropertyFetchByPropertyName(ClassMethod $setUpClassMethod, string $propertyName): void + { + foreach ((array) $setUpClassMethod->stmts as $key => $setupStmt) { + if (! $setupStmt instanceof Expression) { + continue; + } + + if (! $setupStmt->expr instanceof Assign) { + continue; + } + + $assign = $setupStmt->expr; + if (! $assign->var instanceof PropertyFetch) { + continue; + } + + $propertyFetch = $assign->var; + if (! $this->isName($propertyFetch->name, $propertyName)) { + continue; + } + + unset($setUpClassMethod->stmts[$key]); + } + } + + private function matchSoleStubPropertyItem(Property $property): ?PropertyItem + { + if (count($property->props) > 1) { + return null; + } + + // we need some type + if (! $property->type instanceof Node) { + return null; + } + + if (! $this->isObjectType($property->type, new ObjectType(PHPUnitClassName::STUB))) { + return null; + } + + return $property->props[0]; + } +} diff --git a/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php b/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php index c9715ae8..6b2178a8 100644 --- a/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php +++ b/rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php @@ -4,13 +4,13 @@ namespace Rector\PHPUnit\PHPUnit120\Rector\CallLike; -use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node; use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; +use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\ClassMethod;