From 8697b1be2dc0202dfebb1f691a7291ad6e6e8d14 Mon Sep 17 00:00:00 2001 From: Marcin Michalski Date: Tue, 28 Nov 2023 01:26:57 +0000 Subject: [PATCH 1/2] Add phpstan rule enforcing strict mocking --- README.md | 2 +- composer.json | 15 ++-- composer.lock | 2 +- phpstan.neon | 7 ++ phpstan/rules.neon | 23 +++++ phpunit.xml.dist | 4 + src/Phpstan/Rule/EnforceStrictMocking.php | 87 +++++++++++++++++++ .../phpstan/Rule/EnforceExtendedClassTest.php | 73 ++++++++++++++++ .../data/IndirectStrictMockingTraitTest.php | 7 ++ .../phpstan/data/IndirectlyExtendingTest.php | 7 ++ .../data/StrictMockingTestCaseTest.php | 9 ++ tests/phpstan/data/StrictMockingTraitTest.php | 13 +++ tests/phpstan/data/TestCaseTest.php | 9 ++ 13 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 phpstan/rules.neon create mode 100644 src/Phpstan/Rule/EnforceStrictMocking.php create mode 100644 tests/phpstan/Rule/EnforceExtendedClassTest.php create mode 100644 tests/phpstan/data/IndirectStrictMockingTraitTest.php create mode 100644 tests/phpstan/data/IndirectlyExtendingTest.php create mode 100644 tests/phpstan/data/StrictMockingTestCaseTest.php create mode 100644 tests/phpstan/data/StrictMockingTraitTest.php create mode 100644 tests/phpstan/data/TestCaseTest.php diff --git a/README.md b/README.md index 3e37027..7425e2d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ Lendable PHPUnit Extensions -======================== +=========================== > [!WARNING] > This library is still in early development. diff --git a/composer.json b/composer.json index 6ed2cc7..5278ebf 100644 --- a/composer.json +++ b/composer.json @@ -15,13 +15,13 @@ }, "require-dev": { "ergebnis/composer-normalize": "^2.39", + "lendable/composer-license-checker": "^1.0.4", + "php-cs-fixer/shim": "^3.40.0", "phpstan/phpstan": "^1.10.46", - "phpstan/phpstan-phpunit": "^1.3.15", - "rector/rector": "0.18.11", "phpstan/phpstan-deprecation-rules": "^1.1.4", + "phpstan/phpstan-phpunit": "^1.3.15", "phpstan/phpstan-strict-rules": "^1.5.2", - "lendable/composer-license-checker": "^1.0.4", - "php-cs-fixer/shim": "^3.40.0" + "rector/rector": "0.18.11" }, "minimum-stability": "stable", "autoload": { @@ -32,6 +32,7 @@ "autoload-dev": { "psr-4": { "Tests\\Fixtures\\Lendable\\PHPUnitExtensions\\": "tests/fixtures/", + "Tests\\Phpstan\\Lendable\\PHPUnitExtensions\\": "tests/phpstan/", "Tests\\Unit\\Lendable\\PHPUnitExtensions\\": "tests/unit/" } }, @@ -61,6 +62,9 @@ "phpstan": [ "phpstan analyse --ansi --no-progress --memory-limit=-1" ], + "phpunit:phpstan": [ + "phpunit --colors --testsuite=phpstan" + ], "phpunit:unit": [ "phpunit --colors --testsuite=unit" ], @@ -77,7 +81,8 @@ "@rector:check" ], "tests": [ - "@tests:unit" + "@tests:unit", + "@phpunit:phpstan" ], "tests:unit": [ "@phpunit:unit" diff --git a/composer.lock b/composer.lock index 28ec73a..f5e6fe0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f375268e53e561f496b7b3cb0e40de72", + "content-hash": "98eddc06ec21abe7dce470eccdd14de6", "packages": [ { "name": "myclabs/deep-copy", diff --git a/phpstan.neon b/phpstan.neon index b87a965..001b146 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,7 @@ includes: - vendor/phpstan/phpstan-deprecation-rules/rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon - phar://vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon + - phpstan/rules.neon parameters: tmpDir: tmp/phpstan @@ -11,3 +12,9 @@ parameters: - tests level: max checkExplicitMixed: true + excludePaths: + - %currentWorkingDirectory%/tests/phpstan + lendable_phpunit: + enforceStrictMocking: + pardoned: + - Tests\Unit\Lendable\PHPUnitExtensions\TestCaseTest diff --git a/phpstan/rules.neon b/phpstan/rules.neon new file mode 100644 index 0000000..9d15959 --- /dev/null +++ b/phpstan/rules.neon @@ -0,0 +1,23 @@ +conditionalTags: + Lendable\PHPUnitExtensions\Phpstan\Rule\EnforceStrictMocking: + phpstan.rules.rule: %lendable_phpunit.enforceStrictMocking.enabled% + +parametersSchema: + lendable_phpunit: structure([ + enforceStrictMocking: structure([ + enabled: bool() + pardoned: listOf(string()) + ]) + ]) + +parameters: + lendable_phpunit: + enforceStrictMocking: + enabled: true + pardoned: [] + +services: + - + class: Lendable\PHPUnitExtensions\Phpstan\Rule\EnforceStrictMocking + arguments: + pardoned: %lendable_phpunit.enforceStrictMocking.pardoned% diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3ca1769..bd3b7e1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -17,5 +17,9 @@ ./tests/unit/ + + ./tests/phpstan/ + ./tests/phpstan/data/ + diff --git a/src/Phpstan/Rule/EnforceStrictMocking.php b/src/Phpstan/Rule/EnforceStrictMocking.php new file mode 100644 index 0000000..7c02d61 --- /dev/null +++ b/src/Phpstan/Rule/EnforceStrictMocking.php @@ -0,0 +1,87 @@ + + */ +final class EnforceStrictMocking implements Rule +{ + /** + * @var array + */ + private readonly array $pardoned; + + /** + * @param list $pardoned + */ + public function __construct(array $pardoned) + { + $this->pardoned = \array_flip($pardoned); + } + + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->namespacedName instanceof Name) { + return []; + } + + if (!$node->extends instanceof Name) { + return []; + } + + $className = $node->namespacedName->toString(); + if (!\str_ends_with($className, 'Test')) { + return []; + } + + if (isset($this->pardoned[$className])) { + return []; + } + + $reflection = $scope->resolveTypeByName($node->namespacedName)->getClassReflection(); + if (!$reflection instanceof ClassReflection) { + return []; + } + + $parents = $reflection->getParentClassesNames(); + if (!\in_array(TestCase::class, $parents, true)) { + return []; + } + + if (\in_array(StrictMockingTestCase::class, $parents, true)) { + return []; + } + + if (isset($reflection->getTraits(true)[StrictMockingTrait::class])) { + return []; + } + + $ruleErrorBuilder = RuleErrorBuilder::message(\sprintf( + 'Class "%s" must either extend "%s" or use "%s" trait.', + $className, + StrictMockingTestCase::class, + StrictMockingTrait::class, + )); + + return [$ruleErrorBuilder->build()]; + } +} diff --git a/tests/phpstan/Rule/EnforceExtendedClassTest.php b/tests/phpstan/Rule/EnforceExtendedClassTest.php new file mode 100644 index 0000000..b3172d3 --- /dev/null +++ b/tests/phpstan/Rule/EnforceExtendedClassTest.php @@ -0,0 +1,73 @@ +analyse([__DIR__.'/../data/TestCaseTest.php'], [ + [ + $this->errorMessageFor(TestCaseTest::class), + 9, + ], + ]); + } + + #[Test] + public function reports_test_indirectly_extending_phpunits_test_case(): void + { + $this->analyse([__DIR__.'/../data/IndirectlyExtendingTest.php'], [ + [ + $this->errorMessageFor(IndirectlyExtendingTest::class), + 7, + ], + ]); + } + + #[Test] + public function does_not_report_test_extending_strict_mocking(): void + { + $this->analyse([__DIR__.'/../data/StrictMockingTestCaseTest.php'], []); + } + + #[Test] + public function does_not_report_test_directly_using_strict_mocking_trait(): void + { + $this->analyse([__DIR__.'/../data/StrictMockingTraitTest.php'], []); + } + + #[Test] + public function does_not_report_test_indirectly_using_strict_mocking_trait(): void + { + $this->analyse([__DIR__.'/../data/IndirectStrictMockingTraitTest.php'], []); + } + + protected function getRule(): EnforceStrictMocking + { + return new EnforceStrictMocking([]); + } + + private function errorMessageFor(string $class): string + { + return \sprintf( + 'Class "%s" must either extend "%s" or use "%s" trait.', + $class, + TestCase::class, + StrictMocking::class, + ); + } +} diff --git a/tests/phpstan/data/IndirectStrictMockingTraitTest.php b/tests/phpstan/data/IndirectStrictMockingTraitTest.php new file mode 100644 index 0000000..fdcf8fc --- /dev/null +++ b/tests/phpstan/data/IndirectStrictMockingTraitTest.php @@ -0,0 +1,7 @@ + Date: Sat, 2 Dec 2023 10:07:39 +0000 Subject: [PATCH 2/2] Ignore abstract classes in strict mocking PHPStan rule (#14) --- src/Phpstan/Rule/EnforceStrictMocking.php | 4 ++++ tests/phpstan/Rule/EnforceExtendedClassTest.php | 6 ++++++ tests/phpstan/data/AbstractTestCaseTest.php | 9 +++++++++ 3 files changed, 19 insertions(+) create mode 100644 tests/phpstan/data/AbstractTestCaseTest.php diff --git a/src/Phpstan/Rule/EnforceStrictMocking.php b/src/Phpstan/Rule/EnforceStrictMocking.php index 7c02d61..5742a5d 100644 --- a/src/Phpstan/Rule/EnforceStrictMocking.php +++ b/src/Phpstan/Rule/EnforceStrictMocking.php @@ -48,6 +48,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($node->isAbstract()) { + return []; + } + $className = $node->namespacedName->toString(); if (!\str_ends_with($className, 'Test')) { return []; diff --git a/tests/phpstan/Rule/EnforceExtendedClassTest.php b/tests/phpstan/Rule/EnforceExtendedClassTest.php index b3172d3..8a7d2df 100644 --- a/tests/phpstan/Rule/EnforceExtendedClassTest.php +++ b/tests/phpstan/Rule/EnforceExtendedClassTest.php @@ -27,6 +27,12 @@ public function reports_test_directly_extending_phpunits_test_case(): void ]); } + #[Test] + public function does_not_report_abstract_test_directly_extending_phpunits_test_case(): void + { + $this->analyse([__DIR__.'/../data/AbstractTestCaseTest.php'], []); + } + #[Test] public function reports_test_indirectly_extending_phpunits_test_case(): void { diff --git a/tests/phpstan/data/AbstractTestCaseTest.php b/tests/phpstan/data/AbstractTestCaseTest.php new file mode 100644 index 0000000..e4ab258 --- /dev/null +++ b/tests/phpstan/data/AbstractTestCaseTest.php @@ -0,0 +1,9 @@ +