Skip to content
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
56 changes: 34 additions & 22 deletions src/Rules/PHPUnit/ShouldCallParentMethodsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace PHPStan\Rules\PHPUnit;

use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassMethodNode;
use PHPStan\Rules\Rule;
Expand Down Expand Up @@ -66,43 +69,52 @@ public function processNode(Node $node, Scope $scope): array

/**
* @param Node\Stmt[]|null $stmts
*
*/
private function hasParentClassCall(?array $stmts, string $methodName): bool
{
if ($stmts === null) {
return false;
}

foreach ($stmts as $stmt) {
if (! $stmt instanceof Node\Stmt\Expression) {
continue;
}

if (! $stmt->expr instanceof Node\Expr\StaticCall) {
continue;
}
$visitor = new class ($methodName) extends NodeVisitorAbstract {

if (! $stmt->expr->class instanceof Node\Name) {
continue;
}
private string $methodName;

$class = (string) $stmt->expr->class;
public bool $found = false;

if (strtolower($class) !== 'parent') {
continue;
public function __construct(string $methodName)
{
$this->methodName = $methodName;
}

if (! $stmt->expr->name instanceof Node\Identifier) {
continue;
public function enterNode(Node $node): ?int
{
if ($node instanceof Node\FunctionLike || $node instanceof Node\Stmt\ClassLike) {
return NodeVisitor::DONT_TRAVERSE_CHILDREN;
}

if (
$node instanceof Node\Expr\StaticCall
&& !$node->isFirstClassCallable()
&& $node->class instanceof Node\Name
&& $node->class->toLowerString() === 'parent'
&& $node->name instanceof Node\Identifier
&& $node->name->toLowerString() === $this->methodName
) {
$this->found = true;

return NodeVisitor::STOP_TRAVERSAL;
}

return null;
}

if ($stmt->expr->name->toLowerString() === $methodName) {
return true;
}
}
};

$traverser = new NodeTraverser($visitor);
$traverser->traverse($stmts);

return false;
return $visitor->found;
}

}
31 changes: 31 additions & 0 deletions tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<ShouldCallParentMethodsRule>
Expand Down Expand Up @@ -31,6 +32,36 @@ public function testRule(): void
'Missing call to parent::tearDown() method.',
63,
],
[
'Missing call to parent::setUp() method.',
140,
],
[
'Missing call to parent::tearDown() method.',
147,
],
[
'Missing call to parent::setUp() method.',
155,
],
]);
}

public function testFirstClassCallable(): void
{
if (PHP_VERSION_ID < 80100) {
self::markTestSkipped('First-class callable syntax requires PHP 8.1.');
}

$this->analyse([__DIR__ . '/data/missing-parent-method-calls-first-class-callable.php'], [
[
'Missing call to parent::setUp() method.',
20,
],
[
'Missing call to parent::tearDown() method.',
25,
],
]);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php // lint >= 8.1

namespace MissingParentMethodCallsFirstClassCallable;

use PHPUnit\Framework\TestCase;

class BaseTestCase extends TestCase
{
public function setUp(): void
{
}

public function tearDown(): void
{
}
}

class FirstClassCallableTest extends BaseTestCase
{
public function setUp(): void
{
$callback = parent::setUp(...);
}

public function tearDown(): void
{
$callback = parent::tearDown(...);
}
}

class FirstClassCallableInvokedTest extends BaseTestCase
{
public function setUp(): void
{
$callback = parent::setUp(...);
parent::setUp();
}
}
72 changes: 72 additions & 0 deletions tests/Rules/PHPUnit/data/missing-parent-method-calls.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,75 @@ protected function setUp(): void
}

}

class ParentCallInIfTest extends BaseTestCase
{
private bool $enabled = true;

public function setUp(): void
{
if ($this->enabled) {
parent::setUp();
}
}

public function tearDown(): void
{
if ($this->enabled) {
parent::tearDown();
} else {
parent::tearDown();
}
}
}

class ParentCallInTryFinallyTest extends BaseTestCase
{
private ?object $resource = null;

public function setUp(): void
{
try {
parent::setUp();
} catch (\Throwable $e) {
throw $e;
}
}

public function tearDown(): void
{
try {
$this->resource = null;
} finally {
parent::tearDown();
}
}
}

class ParentCallInClosureTest extends BaseTestCase
{
public function setUp(): void
{
$fn = function (): void {
parent::setUp();
};
}

public function tearDown(): void
{
$fn = fn () => parent::tearDown();
}
}

class ParentCallInAnonymousClassTest extends BaseTestCase
{
public function setUp(): void
{
new class extends BaseTestCase {
public function setUp(): void
{
parent::setUp();
}
};
}
}
Loading