Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PHPStan rule enforcing strict mocking #11

Merged
merged 3 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
ben-challis marked this conversation as resolved.
Show resolved Hide resolved
{
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 {}