diff --git a/phpstan.neon b/phpstan.neon index cb841bf..824d929 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,6 +16,11 @@ services: - Rector\Skipper\Fnmatcher - Rector\Skipper\RealpathMatcher + - + class: BrandEmbassyCodingStandard\PhpStan\Rules\Mockery\TestsExtendMockeryTestCaseRule\TestsExtendMockeryTestCaseRule + tags: + - phpstan.rules.rule + parameters: level: max paths: diff --git a/src/BrandEmbassyCodingStandard/PhpStan/Rules/Mockery/TestsExtendMockeryTestCaseRule/TestsExtendMockeryTestCaseRule.php b/src/BrandEmbassyCodingStandard/PhpStan/Rules/Mockery/TestsExtendMockeryTestCaseRule/TestsExtendMockeryTestCaseRule.php new file mode 100644 index 0000000..76276c1 --- /dev/null +++ b/src/BrandEmbassyCodingStandard/PhpStan/Rules/Mockery/TestsExtendMockeryTestCaseRule/TestsExtendMockeryTestCaseRule.php @@ -0,0 +1,89 @@ + + */ +class TestsExtendMockeryTestCaseRule implements Rule +{ + public function __construct( + private readonly ReflectionProvider $reflectionProvider, + ) { + } + + + public function getNodeType(): string + { + return Class_::class; + } + + + /** + * @param Class_ $node + * + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + if ($node->name === null || $node->isAbstract()) { + return []; + } + + $namespacedName = $node->namespacedName; + + if ($namespacedName === null) { + return []; + } + + $className = $scope->resolveName($namespacedName); + + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $class = $this->reflectionProvider->getClass($className); + + if (!$class->isSubclassOf(TestCase::class)) { + return []; + } + + // Allow these code quality tools test cases as they are not using Mockery + if ($class->isSubclassOf(SlevomatCodingStandardTestCase::class) + || $class->isSubclassOf(AbstractRectorTestCase::class) + || $class->isSubclassOf(RuleTestCase::class)) { + return []; + } + + if ($class->isSubclassOf(MockeryTestCase::class)) { + return []; + } + + $ruleErrorMessage = sprintf( + 'PHPUnit test %s must extend %s (directly or indirectly).', + $class->getName(), + MockeryTestCase::class, + ); + + return [ + RuleErrorBuilder::message($ruleErrorMessage) + ->identifier('mockery.testCase') + ->nonIgnorable() + ->build(), + ]; + } +} diff --git a/src/BrandEmbassyCodingStandard/PhpStan/Rules/Mockery/TestsExtendMockeryTestCaseRule/TestsExtendMockeryTestCaseRuleTest.php b/src/BrandEmbassyCodingStandard/PhpStan/Rules/Mockery/TestsExtendMockeryTestCaseRule/TestsExtendMockeryTestCaseRuleTest.php new file mode 100644 index 0000000..aba47d7 --- /dev/null +++ b/src/BrandEmbassyCodingStandard/PhpStan/Rules/Mockery/TestsExtendMockeryTestCaseRule/TestsExtendMockeryTestCaseRuleTest.php @@ -0,0 +1,90 @@ + + */ +class TestsExtendMockeryTestCaseRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new TestsExtendMockeryTestCaseRule($this->createReflectionProvider()); + } + + + public function testExtendsMockeryTestCase(): void + { + $this->analyse( + [__DIR__ . '/__fixtures__/TestExtendsMockeryTestCase.php'], + [], + ); + } + + + public function testExtendsClassWhichExtendsMockeryTestCase(): void + { + $this->analyse( + [__DIR__ . '/__fixtures__/TestExtendsClassWhichExtendsMockeryTestCase.php'], + [], + ); + } + + + public function testExtendsPhpUnitTestCase(): void + { + $this->analyse( + [__DIR__ . '/__fixtures__/TestExtendsPhpUnitTestCase.php'], + [ + [ + 'PHPUnit test BrandEmbassyCodingStandard\PhpStan\Rules\Mockery\TestsExtendMockeryTestCaseRule\__fixtures__\TestExtendsPhpUnitTestCase must extend Mockery\Adapter\Phpunit\MockeryTestCase (directly or indirectly).', + 7, + ], + ], + ); + } + + + public function testExtendsClassWhichExtendsPhpUnitTestCase(): void + { + $this->analyse( + [__DIR__ . '/__fixtures__/TestExtendsClassWhichExtendsPhpUnitTestCase.php'], + [ + [ + 'PHPUnit test BrandEmbassyCodingStandard\PhpStan\Rules\Mockery\TestsExtendMockeryTestCaseRule\__fixtures__\TestExtendsClassWhichExtendsPhpUnitTestCase must extend Mockery\Adapter\Phpunit\MockeryTestCase (directly or indirectly).', + 5, + ], + ], + ); + } + + + public function testExtendsPhpstanRuleTestCase(): void + { + $this->analyse( + [__DIR__ . '/__fixtures__/TestExtendsPhpstanRuleTestCase.php'], + [], + ); + } + + + public function testExtendsRectorRuleTestCase(): void + { + $this->analyse( + [__DIR__ . '/__fixtures__/TestExtendsRectorRuleTestCase.php'], + [], + ); + } + + + public function testExtendsSlevomatCodingStandardTestCase(): void + { + $this->analyse( + [__DIR__ . '/__fixtures__/TestExtendsSlevomatCodingStandardTestCase.php'], + [], + ); + } +} diff --git a/src/BrandEmbassyCodingStandard/PhpStan/Rules/Mockery/TestsExtendMockeryTestCaseRule/__fixtures__/TestExtendsClassWhichExtendsMockeryTestCase.php b/src/BrandEmbassyCodingStandard/PhpStan/Rules/Mockery/TestsExtendMockeryTestCaseRule/__fixtures__/TestExtendsClassWhichExtendsMockeryTestCase.php new file mode 100644 index 0000000..def7c55 --- /dev/null +++ b/src/BrandEmbassyCodingStandard/PhpStan/Rules/Mockery/TestsExtendMockeryTestCaseRule/__fixtures__/TestExtendsClassWhichExtendsMockeryTestCase.php @@ -0,0 +1,7 @@ +