Merge pull request #14 from weirdan/suppress-missing-constructor

Suppress missing constructor for TestCase descendants
This commit is contained in:
Matthew Brown 2019-02-17 21:38:11 -05:00 committed by GitHub
commit 949e3b235d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 166 additions and 3 deletions

View File

@ -21,6 +21,9 @@ class Plugin implements PluginEntryPointInterface
$psalm->addStubFile(__DIR__ . '/stubs/MockBuilder.php');
$psalm->addStubFile(__DIR__ . '/stubs/InvocationMocker.php');
$psalm->addStubFile(__DIR__ . '/stubs/Prophecy.php');
class_exists(Hooks\TestCaseHandler::class, true);
$psalm->registerHooksFromClass(Hooks\TestCaseHandler::class);
}
private function packageVersionIs(string $package, string $op, string $ref): bool

View File

@ -27,7 +27,8 @@
},
"autoload": {
"psr-4": {
"Psalm\\PhpUnitPlugin\\": ["."]
"Psalm\\PhpUnitPlugin\\": ["."],
"Psalm\\PhpUnitPlugin\\Hooks\\": ["hooks"]
}
},
"autoload-dev": {

69
hooks/TestCaseHandler.php Normal file
View File

@ -0,0 +1,69 @@
<?php
namespace Psalm\PhpUnitPlugin\Hooks;
use PHPUnit\Framework\TestCase;
use PhpParser\Comment\Doc;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use Psalm\Codebase;
use Psalm\DocComment;
use Psalm\FileSource;
use Psalm\Plugin\Hook\AfterClassLikeVisitInterface;
use Psalm\Storage\ClassLikeStorage;
class TestCaseHandler implements AfterClassLikeVisitInterface
{
/**
* {@inheritDoc}
*/
public static function afterClassLikeVisit(
ClassLike $classNode,
ClassLikeStorage $classStorage,
FileSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
) {
if ($codebase->classExtends($classStorage->name, TestCase::class)) {
if (self::hasInitializers($classStorage, $classNode)) {
$classStorage->suppressed_issues[] = 'MissingConstructor';
}
}
}
private static function hasInitializers(ClassLikeStorage $storage, ClassLike $stmt): bool
{
if (isset($storage->methods['setup'])) {
return true;
}
foreach ($storage->methods as $method => $_) {
$stmt_method = $stmt->getMethod($method);
if (!$stmt_method) {
throw new \RuntimeException('Failed to find ' . $method);
}
if (self::isBeforeInitializer($stmt_method)) {
return true;
}
}
return false;
}
private static function isBeforeInitializer(ClassMethod $method): bool
{
/** @var string[] $comments */
$comments = $method->getAttribute('comments', []);
foreach ($comments as $comment) {
if (!$comment instanceof Doc) {
continue;
}
$parsed_comment = DocComment::parse((string)$comment->getReformattedText());
if (isset($parsed_comment['specials']['before'])) {
return true;
}
}
return false;
}
}

View File

@ -7,6 +7,7 @@ Feature: TestCase
Given I have the following code preamble
"""
<?php
namespace NS;
use PHPUnit\Framework\TestCase;
"""
@ -25,8 +26,8 @@ Feature: TestCase
"""
When I run Psalm
Then I see these errors
| Type | Message |
| InvalidArgument | Argument 1 of PHPUnit\Framework\TestCase::expectException expects class-string<Throwable>, MyTestCase::class provided |
| Type | Message |
| InvalidArgument | Argument 1 of PHPUnit\Framework\TestCase::expectException expects class-string<Throwable>, NS\MyTestCase::class provided |
Scenario: TestCase::expectException() accepts throwables
Given I have the following code
@ -41,3 +42,92 @@ Feature: TestCase
"""
When I run Psalm
Then I see no errors
Scenario: Stateful test case with setUp produces no MissingConstructor
Given I have the following code
"""
use Prophecy\Prophecy\ObjectProphecy;
interface I { public function work(): int; }
class MyTestCase extends TestCase
{
/** @var ObjectProphecy<I> */
private $i;
/** @return void */
public function setUp() {
$this->i = $this->prophesize(I::class);
}
/** @return void */
public function testSomething() {
$this->i->work()->willReturn(1);;
$i = $this->i->reveal();
$this->assertEquals(1, $i->work());
}
}
"""
When I run Psalm
Then I see no errors
Scenario: Stateful test case with @before produces no MissingConstructor
Given I have the following code
"""
use Prophecy\Prophecy\ObjectProphecy;
interface I { public function work(): int; }
class MyTestCase extends TestCase
{
/** @var ObjectProphecy<I> */
private $i;
/**
* @before
* @return void
*/
public function myInit() {
$this->i = $this->prophesize(I::class);
}
/** @return void */
public function testSomething() {
$this->i->work()->willReturn(1);;
$i = $this->i->reveal();
$this->assertEquals(1, $i->work());
}
}
"""
When I run Psalm
Then I see no errors
Scenario: Stateful test case without @before or setUp produces MissingConstructor
Given I have the following code
"""
use Prophecy\Prophecy\ObjectProphecy;
interface I { public function work(): int; }
class MyTestCase extends TestCase
{
/** @var ObjectProphecy<I> */
private $i;
/** @return void */
public function myInit() {
$this->i = $this->prophesize(I::class);
}
/** @return void */
public function testSomething() {
$this->i->work()->willReturn(1);;
$i = $this->i->reveal();
$this->assertEquals(1, $i->work());
}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| MissingConstructor | NS\MyTestCase has an uninitialized variable $this->i, but no constructor |