From 5a6c73d698288ee5e03cb1818c2991ffccd16d41 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 2 Feb 2026 12:33:50 +0100 Subject: [PATCH 1/2] [code-quality] Add ConfiguredMockEntityToSetterObjectRector --- config/sets/phpunit-code-quality.php | 3 + ...uredMockEntityToSetterObjectRectorTest.php | 28 +++ .../Fixture/fixture.php.inc | 34 ++++ .../Fixture/skip_non_entity.php.inc | 15 ++ .../Source/SomeEntityToBeConfigured.php | 21 ++ .../Source/SomeObjectToBeConfigured.php | 18 ++ .../config/configured_rule.php | 10 + ...nfiguredMockEntityToSetterObjectRector.php | 188 ++++++++++++++++++ stubs/Doctrine/ORM/Mapping/Entity.php | 12 ++ 9 files changed, 329 insertions(+) create mode 100644 rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/ConfiguredMockEntityToSetterObjectRectorTest.php create mode 100644 rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Fixture/fixture.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Fixture/skip_non_entity.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Source/SomeEntityToBeConfigured.php create mode 100644 rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Source/SomeObjectToBeConfigured.php create mode 100644 rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/config/configured_rule.php create mode 100644 rules/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector.php create mode 100644 stubs/Doctrine/ORM/Mapping/Entity.php diff --git a/config/sets/phpunit-code-quality.php b/config/sets/phpunit-code-quality.php index 122b75bc..27ee9b5d 100644 --- a/config/sets/phpunit-code-quality.php +++ b/config/sets/phpunit-code-quality.php @@ -125,6 +125,9 @@ SimplerWithIsInstanceOfRector::class, DirectInstanceOverMockArgRector::class, + // @test first, enable later + // \Rector\PHPUnit\CodeQuality\Rector\Expression\ConfiguredMockEntityToSetterObjectRector::class, + FinalizeTestCaseClassRector::class, DeclareStrictTypesTestsRector::class, WithCallbackIdenticalToStandaloneAssertsRector::class, diff --git a/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/ConfiguredMockEntityToSetterObjectRectorTest.php b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/ConfiguredMockEntityToSetterObjectRectorTest.php new file mode 100644 index 00000000..05bb0767 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/ConfiguredMockEntityToSetterObjectRectorTest.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/Expression/ConfiguredMockEntityToSetterObjectRector/Fixture/fixture.php.inc b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..8d68759d --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Fixture/fixture.php.inc @@ -0,0 +1,34 @@ +createConfiguredMock(SomeEntityToBeConfigured::class, [ + 'getName'=> 'John', + ]); + } +} + +?> +----- +setName('John'); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Fixture/skip_non_entity.php.inc b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Fixture/skip_non_entity.php.inc new file mode 100644 index 00000000..8d38d375 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Fixture/skip_non_entity.php.inc @@ -0,0 +1,15 @@ +createConfiguredMock(SomeObjectToBeConfigured::class, [ + 'getName'=> 'John', + ]); + } +} diff --git a/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Source/SomeEntityToBeConfigured.php b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Source/SomeEntityToBeConfigured.php new file mode 100644 index 00000000..b752ec72 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Source/SomeEntityToBeConfigured.php @@ -0,0 +1,21 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Source/SomeObjectToBeConfigured.php b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Source/SomeObjectToBeConfigured.php new file mode 100644 index 00000000..c4aff6ad --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/Source/SomeObjectToBeConfigured.php @@ -0,0 +1,18 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/config/configured_rule.php b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/config/configured_rule.php new file mode 100644 index 00000000..e9afb3c3 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(ConfiguredMockEntityToSetterObjectRector::class); +}; diff --git a/rules/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector.php b/rules/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector.php new file mode 100644 index 00000000..7ebb9d85 --- /dev/null +++ b/rules/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector.php @@ -0,0 +1,188 @@ +createConfiguredMock(SomeObject::class, [ + 'name' => 'John', + 'surname' => 'Doe', + ]); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestClass +{ + public function test() + { + $someObject = new SomeObject(); + $someObject->setName('John'); + $someObject->setSurname('Doe'); + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Expression::class]; + } + + /** + * @param Expression $node + * @return Expression[]|null + */ + public function refactor(Node $node): ?array + { + if (! $this->testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + if (! $node->expr instanceof Assign) { + return null; + } + + $assign = $node->expr; + if (! $assign->expr instanceof MethodCall) { + return null; + } + + $objectVariable = $assign->var; + + $methodCall = $assign->expr; + if (! $this->isName($methodCall->name, 'createConfiguredMock')) { + return null; + } + + if ($methodCall->isFirstClassCallable()) { + return null; + } + + $mockedClassArg = $methodCall->getArgs()[0]; + + $doctrineClass = $this->matchDoctrineClassName($mockedClassArg->value); + if (! is_string($doctrineClass)) { + return null; + } + + $definedGettersArg = $methodCall->getArgs()[1]; + if (! $definedGettersArg->value instanceof Array_) { + return null; + } + + $assign->expr = new New_(new FullyQualified($doctrineClass)); + + $setterExpressions = $this->createEntitySetterExpressions($definedGettersArg->value, $objectVariable); + + return array_merge([$node], $setterExpressions); + } + + /** + * @return Expression[] + */ + private function createEntitySetterExpressions(Array_ $definedGettersArray, Expr $expr): array + { + $setterExpressions = []; + + foreach ($definedGettersArray->items as $arrayItem) { + if (! $arrayItem->key instanceof Expr) { + continue; + } + + $getterName = $this->valueResolver->getValue($arrayItem->key); + if (! is_string($getterName)) { + continue; + } + + // remove "get" prefix + if (! str_starts_with($getterName, 'get')) { + continue; + } + + $setterName = 'set' . substr($getterName, 3); + + $setterMethodCall = new MethodCall($expr, $setterName, [new Arg($arrayItem->value)]); + $setterExpressions[] = new Expression($setterMethodCall); + } + + return $setterExpressions; + } + + private function matchDoctrineClassName(Expr $expr): string|null + { + $mockedClassValue = $this->valueResolver->getValue($expr); + if (! is_string($mockedClassValue)) { + return null; + } + + if (! $this->reflectionProvider->hasClass($mockedClassValue)) { + return null; + } + + $mockedClass = $this->astResolver->resolveClassFromName($mockedClassValue); + if (! $mockedClass instanceof Node\Stmt\Class_) { + return null; + } + + if (! $this->doctrineEntityDetector->detect($mockedClass)) { + return null; + } + + return $mockedClassValue; + } +} diff --git a/stubs/Doctrine/ORM/Mapping/Entity.php b/stubs/Doctrine/ORM/Mapping/Entity.php new file mode 100644 index 00000000..30930476 --- /dev/null +++ b/stubs/Doctrine/ORM/Mapping/Entity.php @@ -0,0 +1,12 @@ + Date: Mon, 2 Feb 2026 12:10:31 +0000 Subject: [PATCH 2/2] [rector] Rector fixes --- .../Expression/ConfiguredMockEntityToSetterObjectRector.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rules/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector.php b/rules/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector.php index 7ebb9d85..280214e0 100644 --- a/rules/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector.php +++ b/rules/CodeQuality/Rector/Expression/ConfiguredMockEntityToSetterObjectRector.php @@ -4,6 +4,7 @@ namespace Rector\PHPUnit\CodeQuality\Rector\Expression; +use PhpParser\Node\Stmt\Class_; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr; @@ -175,7 +176,7 @@ private function matchDoctrineClassName(Expr $expr): string|null } $mockedClass = $this->astResolver->resolveClassFromName($mockedClassValue); - if (! $mockedClass instanceof Node\Stmt\Class_) { + if (! $mockedClass instanceof Class_) { return null; }