diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index bfd3169..f272cc5 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -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; @@ -66,7 +69,6 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Stmt[]|null $stmts - * */ private function hasParentClassCall(?array $stmts, string $methodName): bool { @@ -74,35 +76,45 @@ private function hasParentClassCall(?array $stmts, string $methodName): bool 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; } } diff --git a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php index b378c67..76705d1 100644 --- a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php +++ b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -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, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/missing-parent-method-calls-first-class-callable.php b/tests/Rules/PHPUnit/data/missing-parent-method-calls-first-class-callable.php new file mode 100644 index 0000000..1e59803 --- /dev/null +++ b/tests/Rules/PHPUnit/data/missing-parent-method-calls-first-class-callable.php @@ -0,0 +1,38 @@ += 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(); + } +} diff --git a/tests/Rules/PHPUnit/data/missing-parent-method-calls.php b/tests/Rules/PHPUnit/data/missing-parent-method-calls.php index 9aa2525..12d9ce0 100644 --- a/tests/Rules/PHPUnit/data/missing-parent-method-calls.php +++ b/tests/Rules/PHPUnit/data/missing-parent-method-calls.php @@ -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(); + } + }; + } +}