Skip to content

Commit

Permalink
Add PHPStan rule enforcing strict mocking (#11)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben Challis <ben-challis@users.noreply.github.com>
  • Loading branch information
marmichalski and ben-challis committed Dec 2, 2023
1 parent 74051b8 commit 3d04736
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Lendable PHPUnit Extensions
========================
===========================

> [!WARNING]
> This library is still in early development.
Expand Down
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
}
},
Expand Down Expand Up @@ -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"
],
Expand All @@ -77,7 +81,8 @@
"@rector:check"
],
"tests": [
"@tests:unit"
"@tests:unit",
"@phpunit:phpstan"
],
"tests:unit": [
"@phpunit:unit"
Expand Down
7 changes: 7 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -11,3 +12,9 @@ parameters:
- tests
level: max
checkExplicitMixed: true
excludePaths:
- %currentWorkingDirectory%/tests/phpstan
lendable_phpunit:
enforceStrictMocking:
pardoned:
- Tests\Unit\Lendable\PHPUnitExtensions\TestCaseTest
23 changes: 23 additions & 0 deletions phpstan/rules.neon
Original file line number Diff line number Diff line change
@@ -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%
4 changes: 4 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@
<testsuite name="unit">
<directory>./tests/unit/</directory>
</testsuite>
<testsuite name="phpstan">
<directory>./tests/phpstan/</directory>
<exclude>./tests/phpstan/data/</exclude>
</testsuite>
</testsuites>
</phpunit>
91 changes: 91 additions & 0 deletions src/Phpstan/Rule/EnforceStrictMocking.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Lendable\PHPUnitExtensions\Phpstan\Rule;

use Lendable\PHPUnitExtensions\StrictMocking as StrictMockingTrait;
use Lendable\PHPUnitExtensions\TestCase as StrictMockingTestCase;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPUnit\Framework\TestCase;

/**
* @implements Rule<Class_>
*/
final class EnforceStrictMocking implements Rule
{
/**
* @var array<class-string, int>
*/
private readonly array $pardoned;

/**
* @param list<class-string> $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 [];
}

if ($node->isAbstract()) {
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()];
}
}
79 changes: 79 additions & 0 deletions tests/phpstan/Rule/EnforceExtendedClassTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Tests\Phpstan\Lendable\PHPUnitExtensions\Rule;

use Lendable\PHPUnitExtensions\Phpstan\Rule\EnforceStrictMocking;
use Lendable\PHPUnitExtensions\StrictMocking;
use Lendable\PHPUnitExtensions\TestCase;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Tests\Phpstan\Lendable\PHPUnitExtensions\data\IndirectlyExtendingTest;
use Tests\Phpstan\Lendable\PHPUnitExtensions\data\TestCaseTest;

#[CoversClass(EnforceStrictMocking::class)]
final class EnforceExtendedClassTest extends RuleTestCase
{
#[Test]
public function reports_test_directly_extending_phpunits_test_case(): void
{
$this->analyse([__DIR__.'/../data/TestCaseTest.php'], [
[
$this->errorMessageFor(TestCaseTest::class),
9,
],
]);
}

#[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
{
$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,
);
}
}
9 changes: 9 additions & 0 deletions tests/phpstan/data/AbstractTestCaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Tests\Phpstan\Lendable\PHPUnitExtensions\data;

use PHPUnit\Framework\TestCase;

abstract class AbstractTestCaseTest extends TestCase {}
7 changes: 7 additions & 0 deletions tests/phpstan/data/IndirectStrictMockingTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace Tests\Phpstan\Lendable\PHPUnitExtensions\data;

final class IndirectStrictMockingTraitTest extends StrictMockingTraitTest {}
7 changes: 7 additions & 0 deletions tests/phpstan/data/IndirectlyExtendingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace Tests\Phpstan\Lendable\PHPUnitExtensions\data;

class IndirectlyExtendingTest extends TestCaseTest {}
9 changes: 9 additions & 0 deletions tests/phpstan/data/StrictMockingTestCaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Tests\Phpstan\Lendable\PHPUnitExtensions\data;

use Lendable\PHPUnitExtensions\TestCase;

final class StrictMockingTestCaseTest extends TestCase {}
13 changes: 13 additions & 0 deletions tests/phpstan/data/StrictMockingTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Tests\Phpstan\Lendable\PHPUnitExtensions\data;

use Lendable\PHPUnitExtensions\StrictMocking;
use PHPUnit\Framework\TestCase;

class StrictMockingTraitTest extends TestCase
{
use StrictMocking;
}
9 changes: 9 additions & 0 deletions tests/phpstan/data/TestCaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Tests\Phpstan\Lendable\PHPUnitExtensions\data;

use PHPUnit\Framework\TestCase;

class TestCaseTest extends TestCase {}

0 comments on commit 3d04736

Please sign in to comment.