Skip to content

Commit d686c82

Browse files
canvuralondrejmirtes
authored andcommitted
Add ShouldCallParentMethodsRule
1 parent 92fb18f commit d686c82

File tree

4 files changed

+214
-0
lines changed

4 files changed

+214
-0
lines changed

rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ rules:
33
- PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule
44
- PHPStan\Rules\PHPUnit\AssertSameWithCountRule
55
- PHPStan\Rules\PHPUnit\MockMethodCallRule
6+
- PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InClassMethodNode;
8+
use PHPStan\Rules\RuleErrorBuilder;
9+
use PHPUnit\Framework\TestCase;
10+
11+
/**
12+
* @implements \PHPStan\Rules\Rule<InClassMethodNode>
13+
*/
14+
class ShouldCallParentMethodsRule implements \PHPStan\Rules\Rule
15+
{
16+
17+
public function getNodeType(): string
18+
{
19+
return InClassMethodNode::class;
20+
}
21+
22+
public function processNode(Node $node, Scope $scope): array
23+
{
24+
/** @var InClassMethodNode $node */
25+
$node = $node;
26+
27+
if ($scope->getClassReflection() === null) {
28+
return [];
29+
}
30+
31+
if (!$scope->getClassReflection()->isSubclassOf(TestCase::class)) {
32+
return [];
33+
}
34+
35+
$parentClass = $scope->getClassReflection()->getParentClass();
36+
37+
if ($parentClass === false) {
38+
return [];
39+
}
40+
41+
if ($parentClass->getName() === TestCase::class) {
42+
return [];
43+
}
44+
45+
if (!in_array(strtolower($node->getOriginalNode()->name->name), ['setup', 'teardown'], true)) {
46+
return [];
47+
}
48+
49+
$hasParentCall = $this->hasParentClassCall($node->getOriginalNode()->getStmts());
50+
51+
if (!$hasParentCall) {
52+
return [
53+
RuleErrorBuilder::message(
54+
sprintf('Missing call to parent::%s method.', $node->getOriginalNode()->name->name)
55+
)->build(),
56+
];
57+
}
58+
59+
return [];
60+
}
61+
62+
/**
63+
* @param Node\Stmt[]|null $stmts
64+
*
65+
* @return bool
66+
*/
67+
private function hasParentClassCall(?array $stmts): bool
68+
{
69+
if ($stmts === null) {
70+
return false;
71+
}
72+
73+
foreach ($stmts as $stmt) {
74+
if (! $stmt instanceof Node\Stmt\Expression) {
75+
continue;
76+
}
77+
78+
if (! $stmt->expr instanceof Node\Expr\StaticCall) {
79+
continue;
80+
}
81+
82+
if (! $stmt->expr->class instanceof Node\Name) {
83+
continue;
84+
}
85+
86+
$class = (string) $stmt->expr->class;
87+
88+
if (strtolower($class) !== 'parent') {
89+
continue;
90+
}
91+
92+
if (! $stmt->expr->name instanceof Node\Identifier) {
93+
continue;
94+
}
95+
96+
if (in_array(strtolower($stmt->expr->name->name), ['setup', 'teardown'], true)) {
97+
return true;
98+
}
99+
}
100+
101+
return false;
102+
}
103+
104+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PHPStan\Rules\Rule;
6+
7+
/**
8+
* @extends \PHPStan\Testing\RuleTestCase<ShouldCallParentMethodsRule>
9+
*/
10+
class ShouldCallParentMethodsRuleTest extends \PHPStan\Testing\RuleTestCase
11+
{
12+
13+
protected function getRule(): Rule
14+
{
15+
return new ShouldCallParentMethodsRule();
16+
}
17+
18+
public function testRule(): void
19+
{
20+
$this->analyse([__DIR__ . '/data/missing-parent-method-calls.php'], [
21+
[
22+
'Missing call to parent::setUp method.',
23+
32,
24+
],
25+
[
26+
'Missing call to parent::tearDown method.',
27+
63,
28+
],
29+
]);
30+
}
31+
32+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace MissingParentMethodCalls;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
class FooTest extends TestCase
8+
{
9+
public function setUp(): void
10+
{
11+
$this->foo = true;
12+
}
13+
}
14+
15+
class BaseTestCase extends TestCase
16+
{
17+
public function setUp(): void
18+
{
19+
$this->bar = true;
20+
}
21+
22+
public function tearDown(): void
23+
{
24+
$this->bar = null;
25+
}
26+
}
27+
28+
class BazTest extends BaseTestCase
29+
{
30+
private $baz;
31+
32+
public function setUp(): void
33+
{
34+
$this->baz = true;
35+
}
36+
37+
public function baz(): bool
38+
{
39+
return $this->baz;
40+
}
41+
}
42+
43+
class BarBazTest extends BaseTestCase
44+
{
45+
public function setUp(): void
46+
{
47+
parent::setUp();
48+
49+
$this->barBaz = true;
50+
}
51+
}
52+
53+
class FooBarBazTest extends BaseTestCase
54+
{
55+
public function setUp(): void
56+
{
57+
$result = 1 + 1;
58+
parent::setUp();
59+
60+
$this->fooBarBaz = $result;
61+
}
62+
63+
public function tearDown(): void
64+
{
65+
$this->fooBarBaz = null;
66+
}
67+
}
68+
69+
class NormalBaseClass {}
70+
71+
class NormalClass extends NormalBaseClass
72+
{
73+
public function setUp()
74+
{
75+
return true;
76+
}
77+
}

0 commit comments

Comments
 (0)