diff --git a/config/sets/phpunit-code-quality.php b/config/sets/phpunit-code-quality.php index a00cf308e..be67fa3fd 100644 --- a/config/sets/phpunit-code-quality.php +++ b/config/sets/phpunit-code-quality.php @@ -57,6 +57,7 @@ use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\PropertyCreateMockToCreateStubRector; use Rector\PHPUnit\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector; +use Rector\PHPUnit\PHPUnit120\Rector\Property\BareVarToStubIntersectionRector; use Rector\PHPUnit\PHPUnit120\Rector\Property\MockObjectVarToStubRector; use Rector\PHPUnit\PHPUnit60\Rector\MethodCall\GetMockBuilderGetMockToCreateMockRector; use Rector\PHPUnit\PHPUnit90\Rector\MethodCall\ReplaceAtMethodWithDesiredMatcherRector; @@ -138,6 +139,7 @@ ExpressionCreateMockToCreateStubRector::class, PropertyCreateMockToCreateStubRector::class, MockObjectVarToStubRector::class, + BareVarToStubIntersectionRector::class, InlineStubPropertyToCreateStubMethodCallRector::class, // @test first, enable later diff --git a/config/sets/phpunit-mock-to-stub.php b/config/sets/phpunit-mock-to-stub.php index 53fe62409..12efa6790 100644 --- a/config/sets/phpunit-mock-to-stub.php +++ b/config/sets/phpunit-mock-to-stub.php @@ -7,6 +7,7 @@ use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector; use Rector\PHPUnit\PHPUnit120\Rector\Class_\PropertyCreateMockToCreateStubRector; use Rector\PHPUnit\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector; +use Rector\PHPUnit\PHPUnit120\Rector\Property\BareVarToStubIntersectionRector; use Rector\PHPUnit\PHPUnit120\Rector\Property\MockObjectVarToStubRector; return static function (RectorConfig $rectorConfig): void { @@ -17,5 +18,6 @@ ExpressionCreateMockToCreateStubRector::class, PropertyCreateMockToCreateStubRector::class, MockObjectVarToStubRector::class, + BareVarToStubIntersectionRector::class, ]); }; diff --git a/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/assert_compare_context.php.inc b/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/assert_compare_context.php.inc index c58b51445..9b3f4ee90 100644 --- a/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/assert_compare_context.php.inc +++ b/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/assert_compare_context.php.inc @@ -2,7 +2,9 @@ namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture; -final class AssertCompareContext +use Behat\Behat\Context\Context; + +final class AssertCompareContext implements Context { public function some($response) { @@ -17,7 +19,9 @@ final class AssertCompareContext namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture; -final class AssertCompareContext +use Behat\Behat\Context\Context; + +final class AssertCompareContext implements Context { public function some($response) { diff --git a/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/assert_null_compare_context.php.inc b/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/assert_null_compare_context.php.inc index f2c349651..3330f3291 100644 --- a/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/assert_null_compare_context.php.inc +++ b/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/assert_null_compare_context.php.inc @@ -2,7 +2,9 @@ namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture; -final class AssertNullCompareContext +use Behat\Behat\Context\Context; + +final class AssertNullCompareContext implements Context { public function some($response) { @@ -16,7 +18,9 @@ final class AssertNullCompareContext namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture; -final class AssertNullCompareContext +use Behat\Behat\Context\Context; + +final class AssertNullCompareContext implements Context { public function some($response) { diff --git a/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/method_exists_context.php.inc b/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/method_exists_context.php.inc index 59edebfa0..971739146 100644 --- a/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/method_exists_context.php.inc +++ b/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/method_exists_context.php.inc @@ -2,7 +2,9 @@ namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture; -final class MethodExistsContext +use Behat\Behat\Context\Context; + +final class MethodExistsContext implements Context { public function some($response) { @@ -16,7 +18,9 @@ final class MethodExistsContext namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture; -final class MethodExistsContext +use Behat\Behat\Context\Context; + +final class MethodExistsContext implements Context { public function some($response) { diff --git a/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/simple_context.php.inc b/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/simple_context.php.inc index 8c16d4857..96abbb26d 100644 --- a/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/simple_context.php.inc +++ b/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/simple_context.php.inc @@ -2,7 +2,9 @@ namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture; -final class SimpleContext +use Behat\Behat\Context\Context; + +final class SimpleContext implements Context { public function some($response) { @@ -18,7 +20,9 @@ final class SimpleContext namespace Rector\PHPUnit\Tests\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector\Fixture; -final class SimpleContext +use Behat\Behat\Context\Context; + +final class SimpleContext implements Context { public function some($response) { diff --git a/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/skip_non_behat_context_class.php.inc b/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/skip_non_behat_context_class.php.inc new file mode 100644 index 000000000..15944442d --- /dev/null +++ b/rules-tests/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector/Fixture/skip_non_behat_context_class.php.inc @@ -0,0 +1,11 @@ +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/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/bare_var.php.inc b/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/bare_var.php.inc new file mode 100644 index 000000000..d29474897 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/bare_var.php.inc @@ -0,0 +1,31 @@ + +----- + diff --git a/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_already_intersection.php.inc b/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_already_intersection.php.inc new file mode 100644 index 000000000..b8205c437 --- /dev/null +++ b/rules-tests/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector/Fixture/skip_already_intersection.php.inc @@ -0,0 +1,13 @@ +withRules(rules: [BareVarToStubIntersectionRector::class]); diff --git a/rules/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector.php b/rules/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector.php index 0d44e1db9..c0ad88799 100644 --- a/rules/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector.php +++ b/rules/CodeQuality/Rector/FuncCall/AssertFuncCallToPHPUnitAssertRector.php @@ -24,6 +24,7 @@ use Rector\PhpParser\Node\Value\ValueResolver; use Rector\PHPStan\ScopeFetcher; use Rector\PHPUnit\Enum\AssertMethod; +use Rector\PHPUnit\Enum\BehatClassName; use Rector\PHPUnit\Enum\PHPUnitClassName; use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; use Rector\Rector\AbstractRector; @@ -195,15 +196,13 @@ public function refactor(Node $node): ?Class_ private function isBehatContext(Class_ $class): bool { $scope = ScopeFetcher::fetch($class); - if (! $scope->getClassReflection() instanceof ClassReflection) { + $classReflection = $scope->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { return false; } - $className = $scope->getClassReflection() - ->getName(); - // special case with static call - return str_ends_with($className, 'Context'); + return $classReflection->is(BehatClassName::CONTEXT); } /** diff --git a/rules/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector.php b/rules/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector.php new file mode 100644 index 000000000..614715ecb --- /dev/null +++ b/rules/PHPUnit120/Rector/Property/BareVarToStubIntersectionRector.php @@ -0,0 +1,142 @@ +testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + // only properties already converted to a Stub native type + if (! $this->isStubNativeType($node->type)) { + return null; + } + + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + if (! $phpDocInfo instanceof PhpDocInfo) { + return null; + } + + $varTagValueNode = $phpDocInfo->getVarTagValueNode(); + if (! $varTagValueNode instanceof VarTagValueNode) { + return null; + } + + if (! $this->addStubIntersection($varTagValueNode)) { + return null; + } + + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Add a &Stub intersection to a bare single-class @var docblock of a property changed to a Stub native type', + [ + new CodeSample( + <<<'CODE_SAMPLE' +/** + * @var FormBuilderInterface + */ +private \PHPUnit\Framework\MockObject\Stub $formBuilder; +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +/** + * @var FormBuilderInterface&Stub + */ +private \PHPUnit\Framework\MockObject\Stub $formBuilder; +CODE_SAMPLE + ), + ] + ); + } + + private function isStubNativeType(?Node $typeNode): bool + { + if (! $typeNode instanceof Node) { + return false; + } + + if ($typeNode instanceof IntersectionType) { + return array_any($typeNode->types, fn (Identifier|Name $innerType): bool => $this->isStubName($innerType)); + } + + return $this->isStubName($typeNode); + } + + private function isStubName(?Node $node): bool + { + return $node instanceof Node && $this->getName($node) === PHPUnitClassName::STUB; + } + + private function addStubIntersection(VarTagValueNode $varTagValueNode): bool + { + $typeNode = $varTagValueNode->type; + + // only a single bare class type, not already a union/intersection + if (! $typeNode instanceof IdentifierTypeNode) { + return false; + } + + // skip Stub/MockObject themselves, only mocked class types + if (in_array($this->resolveShortName($typeNode->name), ['Stub', 'MockObject'], true)) { + return false; + } + + $varTagValueNode->type = new BracketsAwareIntersectionTypeNode([$typeNode, new IdentifierTypeNode('Stub')]); + + return true; + } + + private function resolveShortName(string $name): string + { + $lastBackslashPosition = strrpos($name, '\\'); + + return $lastBackslashPosition === false ? $name : substr($name, $lastBackslashPosition + 1); + } +}