From 598caf33eab1bbaf17bf3e12b7add29eea4ac041 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 29 Sep 2023 09:59:21 +0200 Subject: [PATCH] [PHPUnit] Add NoTestMocksRule --- docs/rules_overview.md | 40 +++++- phpstan.neon | 1 + src/Rules/PHPUnit/NoTestMocksRule.php | 136 ++++++++++++++++++ .../Fixture/ReturnFalseOnly.php | 2 +- .../Fixture/SkipReturnBool.php | 2 +- .../NoTestMocksRule/Fixture/SkipApiMock.php | 16 +++ .../NoTestMocksRule/Fixture/SomeMocking.php | 15 ++ .../NoTestMocksRule/NoTestMocksRuleTest.php | 45 ++++++ .../Source/SomeAllowedType.php | 7 + .../config/configured_rule.neon | 7 + 10 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 src/Rules/PHPUnit/NoTestMocksRule.php create mode 100644 tests/Rules/PHPUnit/NoTestMocksRule/Fixture/SkipApiMock.php create mode 100644 tests/Rules/PHPUnit/NoTestMocksRule/Fixture/SomeMocking.php create mode 100644 tests/Rules/PHPUnit/NoTestMocksRule/NoTestMocksRuleTest.php create mode 100644 tests/Rules/PHPUnit/NoTestMocksRule/Source/SomeAllowedType.php create mode 100644 tests/Rules/PHPUnit/NoTestMocksRule/config/configured_rule.neon diff --git a/docs/rules_overview.md b/docs/rules_overview.md index 7eb0d642..d58274ec 100644 --- a/docs/rules_overview.md +++ b/docs/rules_overview.md @@ -1,4 +1,4 @@ -# 56 Rules Overview +# 57 Rules Overview ## AnnotateRegexClassConstWithRegexLinkRule @@ -1519,6 +1519,44 @@ final class SomeClass
+## NoTestMocksRule + +Mocking "%s" class is forbidden. Use direct/anonymous class instead for better static analysis + +- class: [`Symplify\PHPStanRules\Rules\PHPUnit\NoTestMocksRule`](../src/Rules/PHPUnit/NoTestMocksRule.php) + +```php +use PHPUnit\Framework\TestCase; + +final class SkipApiMock extends TestCase +{ + public function test() + { + $someTypeMock = $this->createMock(SomeType::class); + } +} +``` + +:x: + +
+ +```php +use PHPUnit\Framework\TestCase; + +final class SkipApiMock extends TestCase +{ + public function test() + { + $someTypeMock = new class() implements SomeType {}; + } +} +``` + +:+1: + +
+ ## NoVoidGetterMethodRule Getter method must return something, not void diff --git a/phpstan.neon b/phpstan.neon index 1cd7c39c..f2348a4d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -56,3 +56,4 @@ parameters: # part of public contract - '#Public constant "Symplify\\PHPStanRules\\(.*?)\:\:ERROR_MESSAGE" is never used#' + - '#Method Symplify\\PHPStanRules\\Tests\\Rules\\PHPUnit\\(.*?)\\(.*?)Test\:\:testRule\(\) has parameter \$expectedErrorMessagesWithLines with no value type specified in iterable type array#' diff --git a/src/Rules/PHPUnit/NoTestMocksRule.php b/src/Rules/PHPUnit/NoTestMocksRule.php new file mode 100644 index 00000000..07a22df0 --- /dev/null +++ b/src/Rules/PHPUnit/NoTestMocksRule.php @@ -0,0 +1,136 @@ + + */ +final class NoTestMocksRule implements Rule, DocumentedRuleInterface +{ + /** + * @api + * @var string + */ + public const ERROR_MESSAGE = 'Mocking "%s" class is forbidden. Use direct/anonymous class instead for better static analysis'; + + /** + * @var string[] + */ + private const MOCKING_METHOD_NAMES = ['createMock', 'createPartialMock', 'createConfiguredMock', 'createStub']; + + /** + * @param string[] $allowedTypes + */ + public function __construct( + private array $allowedTypes = [] + ) { + } + + /** + * @return class-string + */ + public function getNodeType(): string + { + return MethodCall::class; + } + + /** + * @param MethodCall $node + */ + public function processNode(Node $node, Scope $scope): array + { + if (! $node->name instanceof Identifier) { + return []; + } + + $methodName = $node->name->toString(); + if (! in_array($methodName, self::MOCKING_METHOD_NAMES, true)) { + return []; + } + + $mockedObjectType = $this->resolveMockedObjectType($node, $scope); + if (! $mockedObjectType instanceof ObjectType) { + return []; + } + + if ($this->isAllowedType($mockedObjectType)) { + return []; + } + + $errorMessage = sprintf(self::ERROR_MESSAGE, $mockedObjectType->getClassName()); + + return [$errorMessage]; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition(self::ERROR_MESSAGE, [ + new CodeSample( + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SkipApiMock extends TestCase +{ + public function test() + { + $someTypeMock = $this->createMock(SomeType::class); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SkipApiMock extends TestCase +{ + public function test() + { + $someTypeMock = new class() implements SomeType {}; + } +} +CODE_SAMPLE + ), + ]); + } + + private function resolveMockedObjectType(MethodCall $methodCall, Scope $scope): ?ObjectType + { + $args = $methodCall->getArgs(); + + $mockedArgValue = $args[0]->value; + $variableType = $scope->getType($mockedArgValue); + + foreach ($variableType->getConstantStrings() as $constantString) { + return new ObjectType($constantString->getValue()); + } + + return null; + } + + private function isAllowedType(ObjectType $objectType): bool + { + foreach ($this->allowedTypes as $allowedType) { + if ($objectType->getClassName() === $allowedType) { + return true; + } + + if ($objectType->isInstanceOf($allowedType)->yes()) { + return true; + } + } + + return false; + } +} diff --git a/tests/Rules/NarrowType/NoReturnFalseInNonBoolClassMethodRule/Fixture/ReturnFalseOnly.php b/tests/Rules/NarrowType/NoReturnFalseInNonBoolClassMethodRule/Fixture/ReturnFalseOnly.php index 33cd0428..a09500d5 100644 --- a/tests/Rules/NarrowType/NoReturnFalseInNonBoolClassMethodRule/Fixture/ReturnFalseOnly.php +++ b/tests/Rules/NarrowType/NoReturnFalseInNonBoolClassMethodRule/Fixture/ReturnFalseOnly.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Ares\PHPStan\Tests\Rule\NoReturnFalseInNonBoolClassMethodRule\Fixture; +namespace Symplify\PHPStanRules\Tests\Rule\NoReturnFalseInNonBoolClassMethodRule\Fixture; final class ReturnFalseOnly { diff --git a/tests/Rules/NarrowType/NoReturnFalseInNonBoolClassMethodRule/Fixture/SkipReturnBool.php b/tests/Rules/NarrowType/NoReturnFalseInNonBoolClassMethodRule/Fixture/SkipReturnBool.php index 91c2b67d..415bdd41 100644 --- a/tests/Rules/NarrowType/NoReturnFalseInNonBoolClassMethodRule/Fixture/SkipReturnBool.php +++ b/tests/Rules/NarrowType/NoReturnFalseInNonBoolClassMethodRule/Fixture/SkipReturnBool.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Ares\PHPStan\Tests\Rule\NoReturnFalseInNonBoolClassMethodRule\Fixture; +namespace Symplify\PHPStanRules\Tests\Rule\NoReturnFalseInNonBoolClassMethodRule\Fixture; final class SkipReturnBool { diff --git a/tests/Rules/PHPUnit/NoTestMocksRule/Fixture/SkipApiMock.php b/tests/Rules/PHPUnit/NoTestMocksRule/Fixture/SkipApiMock.php new file mode 100644 index 00000000..789634fb --- /dev/null +++ b/tests/Rules/PHPUnit/NoTestMocksRule/Fixture/SkipApiMock.php @@ -0,0 +1,16 @@ +createMock(SomeAllowedType::class); + } +} diff --git a/tests/Rules/PHPUnit/NoTestMocksRule/Fixture/SomeMocking.php b/tests/Rules/PHPUnit/NoTestMocksRule/Fixture/SomeMocking.php new file mode 100644 index 00000000..66df9c7d --- /dev/null +++ b/tests/Rules/PHPUnit/NoTestMocksRule/Fixture/SomeMocking.php @@ -0,0 +1,15 @@ +createMock('SomeClass'); + } +} diff --git a/tests/Rules/PHPUnit/NoTestMocksRule/NoTestMocksRuleTest.php b/tests/Rules/PHPUnit/NoTestMocksRule/NoTestMocksRuleTest.php new file mode 100644 index 00000000..9cd4738a --- /dev/null +++ b/tests/Rules/PHPUnit/NoTestMocksRule/NoTestMocksRuleTest.php @@ -0,0 +1,45 @@ +analyse([$filePath], $expectedErrorMessagesWithLines); + } + + public static function provideData(): Iterator + { + yield [ + __DIR__ . '/Fixture/SomeMocking.php', + [[sprintf(NoTestMocksRule::ERROR_MESSAGE, 'SomeClass'), 13]], + ]; + + yield [__DIR__ . '/Fixture/SkipApiMock.php', []]; + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/config/configured_rule.neon', + ]; + } + + protected function getRule(): Rule + { + return self::getContainer()->getByType(NoTestMocksRule::class); + } +} diff --git a/tests/Rules/PHPUnit/NoTestMocksRule/Source/SomeAllowedType.php b/tests/Rules/PHPUnit/NoTestMocksRule/Source/SomeAllowedType.php new file mode 100644 index 00000000..16c9e7fb --- /dev/null +++ b/tests/Rules/PHPUnit/NoTestMocksRule/Source/SomeAllowedType.php @@ -0,0 +1,7 @@ +