Skip to content

Commit 3d04736

Browse files
Add PHPStan rule enforcing strict mocking (#11)
Co-authored-by: Ben Challis <ben-challis@users.noreply.github.com>
1 parent 74051b8 commit 3d04736

13 files changed

+265
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Lendable PHPUnit Extensions
2-
========================
2+
===========================
33

44
> [!WARNING]
55
> This library is still in early development.

composer.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"autoload-dev": {
3333
"psr-4": {
3434
"Tests\\Fixtures\\Lendable\\PHPUnitExtensions\\": "tests/fixtures/",
35+
"Tests\\Phpstan\\Lendable\\PHPUnitExtensions\\": "tests/phpstan/",
3536
"Tests\\Unit\\Lendable\\PHPUnitExtensions\\": "tests/unit/"
3637
}
3738
},
@@ -61,6 +62,9 @@
6162
"phpstan": [
6263
"phpstan analyse --ansi --no-progress --memory-limit=-1"
6364
],
65+
"phpunit:phpstan": [
66+
"phpunit --colors --testsuite=phpstan"
67+
],
6468
"phpunit:unit": [
6569
"phpunit --colors --testsuite=unit"
6670
],
@@ -77,7 +81,8 @@
7781
"@rector:check"
7882
],
7983
"tests": [
80-
"@tests:unit"
84+
"@tests:unit",
85+
"@phpunit:phpstan"
8186
],
8287
"tests:unit": [
8388
"@phpunit:unit"

phpstan.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ includes:
33
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
44
- vendor/phpstan/phpstan-strict-rules/rules.neon
55
- phar://vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon
6+
- phpstan/rules.neon
67

78
parameters:
89
tmpDir: tmp/phpstan
@@ -11,3 +12,9 @@ parameters:
1112
- tests
1213
level: max
1314
checkExplicitMixed: true
15+
excludePaths:
16+
- %currentWorkingDirectory%/tests/phpstan
17+
lendable_phpunit:
18+
enforceStrictMocking:
19+
pardoned:
20+
- Tests\Unit\Lendable\PHPUnitExtensions\TestCaseTest

phpstan/rules.neon

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
conditionalTags:
2+
Lendable\PHPUnitExtensions\Phpstan\Rule\EnforceStrictMocking:
3+
phpstan.rules.rule: %lendable_phpunit.enforceStrictMocking.enabled%
4+
5+
parametersSchema:
6+
lendable_phpunit: structure([
7+
enforceStrictMocking: structure([
8+
enabled: bool()
9+
pardoned: listOf(string())
10+
])
11+
])
12+
13+
parameters:
14+
lendable_phpunit:
15+
enforceStrictMocking:
16+
enabled: true
17+
pardoned: []
18+
19+
services:
20+
-
21+
class: Lendable\PHPUnitExtensions\Phpstan\Rule\EnforceStrictMocking
22+
arguments:
23+
pardoned: %lendable_phpunit.enforceStrictMocking.pardoned%

phpunit.xml.dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,9 @@
1717
<testsuite name="unit">
1818
<directory>./tests/unit/</directory>
1919
</testsuite>
20+
<testsuite name="phpstan">
21+
<directory>./tests/phpstan/</directory>
22+
<exclude>./tests/phpstan/data/</exclude>
23+
</testsuite>
2024
</testsuites>
2125
</phpunit>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Lendable\PHPUnitExtensions\Phpstan\Rule;
6+
7+
use Lendable\PHPUnitExtensions\StrictMocking as StrictMockingTrait;
8+
use Lendable\PHPUnitExtensions\TestCase as StrictMockingTestCase;
9+
use PhpParser\Node;
10+
use PhpParser\Node\Name;
11+
use PhpParser\Node\Stmt\Class_;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Reflection\ClassReflection;
14+
use PHPStan\Rules\Rule;
15+
use PHPStan\Rules\RuleErrorBuilder;
16+
use PHPUnit\Framework\TestCase;
17+
18+
/**
19+
* @implements Rule<Class_>
20+
*/
21+
final class EnforceStrictMocking implements Rule
22+
{
23+
/**
24+
* @var array<class-string, int>
25+
*/
26+
private readonly array $pardoned;
27+
28+
/**
29+
* @param list<class-string> $pardoned
30+
*/
31+
public function __construct(array $pardoned)
32+
{
33+
$this->pardoned = \array_flip($pardoned);
34+
}
35+
36+
public function getNodeType(): string
37+
{
38+
return Class_::class;
39+
}
40+
41+
public function processNode(Node $node, Scope $scope): array
42+
{
43+
if (!$node->namespacedName instanceof Name) {
44+
return [];
45+
}
46+
47+
if (!$node->extends instanceof Name) {
48+
return [];
49+
}
50+
51+
if ($node->isAbstract()) {
52+
return [];
53+
}
54+
55+
$className = $node->namespacedName->toString();
56+
if (!\str_ends_with($className, 'Test')) {
57+
return [];
58+
}
59+
60+
if (isset($this->pardoned[$className])) {
61+
return [];
62+
}
63+
64+
$reflection = $scope->resolveTypeByName($node->namespacedName)->getClassReflection();
65+
if (!$reflection instanceof ClassReflection) {
66+
return [];
67+
}
68+
69+
$parents = $reflection->getParentClassesNames();
70+
if (!\in_array(TestCase::class, $parents, true)) {
71+
return [];
72+
}
73+
74+
if (\in_array(StrictMockingTestCase::class, $parents, true)) {
75+
return [];
76+
}
77+
78+
if (isset($reflection->getTraits(true)[StrictMockingTrait::class])) {
79+
return [];
80+
}
81+
82+
$ruleErrorBuilder = RuleErrorBuilder::message(\sprintf(
83+
'Class "%s" must either extend "%s" or use "%s" trait.',
84+
$className,
85+
StrictMockingTestCase::class,
86+
StrictMockingTrait::class,
87+
));
88+
89+
return [$ruleErrorBuilder->build()];
90+
}
91+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Phpstan\Lendable\PHPUnitExtensions\Rule;
6+
7+
use Lendable\PHPUnitExtensions\Phpstan\Rule\EnforceStrictMocking;
8+
use Lendable\PHPUnitExtensions\StrictMocking;
9+
use Lendable\PHPUnitExtensions\TestCase;
10+
use PHPStan\Testing\RuleTestCase;
11+
use PHPUnit\Framework\Attributes\CoversClass;
12+
use PHPUnit\Framework\Attributes\Test;
13+
use Tests\Phpstan\Lendable\PHPUnitExtensions\data\IndirectlyExtendingTest;
14+
use Tests\Phpstan\Lendable\PHPUnitExtensions\data\TestCaseTest;
15+
16+
#[CoversClass(EnforceStrictMocking::class)]
17+
final class EnforceExtendedClassTest extends RuleTestCase
18+
{
19+
#[Test]
20+
public function reports_test_directly_extending_phpunits_test_case(): void
21+
{
22+
$this->analyse([__DIR__.'/../data/TestCaseTest.php'], [
23+
[
24+
$this->errorMessageFor(TestCaseTest::class),
25+
9,
26+
],
27+
]);
28+
}
29+
30+
#[Test]
31+
public function does_not_report_abstract_test_directly_extending_phpunits_test_case(): void
32+
{
33+
$this->analyse([__DIR__.'/../data/AbstractTestCaseTest.php'], []);
34+
}
35+
36+
#[Test]
37+
public function reports_test_indirectly_extending_phpunits_test_case(): void
38+
{
39+
$this->analyse([__DIR__.'/../data/IndirectlyExtendingTest.php'], [
40+
[
41+
$this->errorMessageFor(IndirectlyExtendingTest::class),
42+
7,
43+
],
44+
]);
45+
}
46+
47+
#[Test]
48+
public function does_not_report_test_extending_strict_mocking(): void
49+
{
50+
$this->analyse([__DIR__.'/../data/StrictMockingTestCaseTest.php'], []);
51+
}
52+
53+
#[Test]
54+
public function does_not_report_test_directly_using_strict_mocking_trait(): void
55+
{
56+
$this->analyse([__DIR__.'/../data/StrictMockingTraitTest.php'], []);
57+
}
58+
59+
#[Test]
60+
public function does_not_report_test_indirectly_using_strict_mocking_trait(): void
61+
{
62+
$this->analyse([__DIR__.'/../data/IndirectStrictMockingTraitTest.php'], []);
63+
}
64+
65+
protected function getRule(): EnforceStrictMocking
66+
{
67+
return new EnforceStrictMocking([]);
68+
}
69+
70+
private function errorMessageFor(string $class): string
71+
{
72+
return \sprintf(
73+
'Class "%s" must either extend "%s" or use "%s" trait.',
74+
$class,
75+
TestCase::class,
76+
StrictMocking::class,
77+
);
78+
}
79+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Phpstan\Lendable\PHPUnitExtensions\data;
6+
7+
use PHPUnit\Framework\TestCase;
8+
9+
abstract class AbstractTestCaseTest extends TestCase {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Phpstan\Lendable\PHPUnitExtensions\data;
6+
7+
final class IndirectStrictMockingTraitTest extends StrictMockingTraitTest {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Phpstan\Lendable\PHPUnitExtensions\data;
6+
7+
class IndirectlyExtendingTest extends TestCaseTest {}

0 commit comments

Comments
 (0)