A PHPUnit plugin for Psalm (requires Psalm v4+).
Installation:
composer require --dev psalm/plugin-phpunit
vendor/bin/psalm-plugin enable psalm/plugin-phpunit
A PHPUnit plugin for Psalm
A PHPUnit plugin for Psalm (requires Psalm v4+).
Installation:
composer require --dev psalm/plugin-phpunit
vendor/bin/psalm-plugin enable psalm/plugin-phpunit
Hi,
We have a lot of tests in our Symfony project which basically look like this
class SomeServiceTest extends TestCase
{
use ProphecyTrait;
/**
* @var OtherServiceInterface|ObjectProphecy
*/
private $otherService;
private SomeService $someService;
protected function setUp(): void
{
$this->otherService = $this->prophesize(OtherServiceInterface::class);
$this->someService = new SomeService(
$this-otherService->reveal()
);
}
...
}
But in tests we get error when we try to use willReturn()
method from Prophecy
$this-otherService->method()->willReturn($something);
we get:
psalm: PossiblyUndefinedMethod: Method OtherService::willReturn does not exist
Isn't the union type in the annotation states that both classes are valid for this property?
According to PHPUnit docs:
A data provider method must be public and either return an array of arrays or an object that implements the Iterator interface and yields an array for each iteration step.
I'm returning Generator
/** @return Generator<list<mixed>> */
public function providerX() : Generator
plugin complains that
ERROR: MixedInferredReturnType - tests/MyTest.php:xx:yy - Providers must return iterable<array-key, array<array-key, mixed>>, possibly different Generator<mixed, list, mixed, mixed> provided (see https://psalm.dev/047)
I'd expect no error for this.
Please add support for phpunit 8.
MissingThrowsDocblock<T>
currently bubbles up when preceding code invokes TestCase::expectException(T::class)
I'm reasonably certain phpunit doesn't let InvalidArgumentException
bubble up if I'm calling static::expectException(InvalidArgumentException::class)
While working on a project that uses very strict psalm settings and this plugin, we discovered that running vendor/bin/psalm
with following configuration and no arguments, no issues are reported:
<?xml version="1.0"?>
<psalm
totallyTyped="true"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src"/>
<directory name="tests/src"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<InternalMethod>
<errorLevel type="suppress">
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::willReturnCallback"/>
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::willReturn"/>
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::method"/>
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::with"/>
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::withConsecutive"/>
</errorLevel>
</InternalMethod>
</issueHandlers>
<plugins>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
</plugins>
</psalm>
If I run vendor/bin/psalm tests
(new argument added!), the output changes, and starts reporting stuff that is generally silenced by this plugin (the following, but for every test):
ERROR: MissingConstructor - tests/src/Unit/Presentation/UserViewTest.php:27:35 - SomeProject\Core\Tests\Unit\Presentation\UserViewTest has an uninitialized property $this->user, but no constructor (see https://psalm.dev/073)
private UserSettingRepository $userSettingRepository;
Interestingly, no other errors around assertions nor anything: it seems like the translation from setUp
to __construct
isn't really working as expected ๐ค
Tried it on updated dependencies BTW:
psalm/plugin-phpunit 0.12.2 Psalm plugin for PHPUnit
vimeo/psalm 3.17.2 A static analysis tool for finding errors in PHP applications
https://github.com/sebastianbergmann/phpunit/blob/master/ChangeLog-7.5.md
Psalm could provide type assertions for assertIs*
methods.
This raises the question of how to support version-dependent stubs.
TestCase::$foo
is T|null
TestCase::setUp()
sets TestCase::$foo
to T
TestCase::$foo
TestCase::assertInstanceOf(T::class, $this->foo)
is redundantnot quite sure on the exact error/test case that should occur, but this is a vague mashup of what I'm thinking:
Scenario: setUp resolves types
Given I have the following code
"""
class MyTestCase extends TestCase
{
/** @var \DateTime|null */
private $date;
/** @return void */
public function setUp() {
$this->date = new \DateTime();
}
/** @return void */
public function testSomething() {
$this->assertInstanceOf(\DateTime::class, $this->date);
}
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| RedundantConditionGivenDocblockType | Found a redundant condition when evaluating docblock-defined type $this->date and trying to reconcile type 'DateTime' to DateTime |
And I see no other errors
Is it feasible for this plugin to parse phpunit coverage files to prevent PossiblyUnusedMethod
when calling psalm --find-dead-code
from being flagged up if the method is present as being covered in the coverage file?
I have not found usages of this private
method
psalm-plugin-phpunit/src/Plugin.php
Lines 28 to 38 in 83b5c10
May be it needs to be deleted?
Also if deleting this, also delete composer/semver
dependency composer remove composer/semver
because it's used only there
Consider this case:
<?php
use PHPUnit\Framework\TestCase;
final class JsonDecodeTest extends TestCase
{
public function testThrowsOnInvalidJson(): void
{
$this->expectException(\JsonException::class);
// UnusedFunctionCall is thrown here
json_decode('', flags: JSON_THROW_ON_ERROR);
}
}
I am not sure that this is doable or worth the effort, so feel free to close the issue. For now we just suppressed UnusedFunctionCall
and UnusedMethodCall
in psalm.xml
for the whole tests
directory.
If we use not supported / unknown psalm annotation, for example instead of @psalm-suppress
by mistake we use @psalm-ignore
which is not a valid existing annotation, psalm should report that as an issue rather than throwing exception. Because of exception it is checking whole codebase instead of the files changed in the branch and throws other random errors
[RUNNING] [PHP-DEPENDENCIES] .cache/php/Psalm.x
Checking files
PHP Fatal error: Uncaught Psalm\Exception\DocblockParseException: Unrecognised annotation @psalm-ignore in /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/DocComment.php:156
Stack trace:
#0 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/psalm/plugin-phpunit/hooks/TestCaseHandler.php(528): Psalm\DocComment::parse('This method sea...', 57)
#1 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/psalm/plugin-phpunit/hooks/TestCaseHandler.php(518): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::getSpecials(Object(PhpParser\Node\Stmt\ClassMethod))
#2 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/psalm/plugin-phpunit/hooks/TestCaseHandler.php(509): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::isBeforeInitializer(Object(PhpParser\Node\Stmt\ClassMethod))
#3 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/psalm/plugin-phpunit/hooks/TestCaseHandler.php(90): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::hasInitializers(Object(Psalm\Storage\ClassLikeStorage), Object(PhpParser\Node\Stmt\Class_))
#4 /Users/sourav/Documents/work/ in /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/DocComment.php on line 156
Fatal error: Uncaught Psalm\Exception\DocblockParseException: Unrecognised annotation @psalm-ignore in /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/DocComment.php:156
Stack trace:
#0 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/psalm/plugin-phpunit/hooks/TestCaseHandler.php(528): Psalm\DocComment::parse('This method sea...', 57)
#1 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/psalm/plugin-phpunit/hooks/TestCaseHandler.php(518): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::getSpecials(Object(PhpParser\Node\Stmt\ClassMethod))
#2 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/psalm/plugin-phpunit/hooks/TestCaseHandler.php(509): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::isBeforeInitializer(Object(PhpParser\Node\Stmt\ClassMethod))
#3 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/psalm/plugin-phpunit/hooks/TestCaseHandler.php(90): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::hasInitializers(Object(Psalm\Storage\ClassLikeStorage), Object(PhpParser\Node\Stmt\Class_))
#4 /Users/sourav/Documents/work/ in /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/DocComment.php on line 156
PHP Fatal error: Uncaught InvalidArgumentException: Could not get file storage for /users/sourav/documents/work/vimeo/vimeo/routes/main.php in /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Provider/FileStorageProvider.php:49
Stack trace:
#0 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(141): Psalm\Internal\Provider\FileStorageProvider->get('/users/sourav/d...')
#1 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(262): Psalm\Internal\Analyzer\FileAnalyzer->analyze(NULL)
#2 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php(182): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(10, '/Users/sourav/D...')
#3 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(354): Psalm\Internal\Fork\Pool->__construct(Array, Object(Closure), Object(Closure), Object(Closure), Object(Closure))
#4 / in /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Provider/FileStorageProvider.php on line 49
Fatal error: Uncaught InvalidArgumentException: Could not get file storage for /users/sourav/documents/work/vimeo/vimeo/routes/main.php in /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Provider/FileStorageProvider.php:49
Stack trace:
#0 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(141): Psalm\Internal\Provider\FileStorageProvider->get('/users/sourav/d...')
#1 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(262): Psalm\Internal\Analyzer\FileAnalyzer->analyze(NULL)
#2 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php(182): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(10, '/Users/sourav/D...')
#3 /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(354): Psalm\Internal\Fork\Pool->__construct(Array, Object(Closure), Object(Closure), Object(Closure), Object(Closure))
#4 / in /Users/sourav/Documents/work/Vimeo/vimeo/vendor/vimeo/psalm/src/Psalm/Internal/Provider/FileStorageProvider.php on line 49
This code is valid https://psalm.dev/r/e1d61bda01
But when I use assertArrayHasKey
and assertCount
, the offset are reported as possibly undefined.
It would be great to tell psalm that
assertArrayHasKey work as `array_key_exists` so the offset is not undefined for arrays
assertCount work as `count` so the offset is not undefined for list
I'm not familiar with, but I'm not sure it can be done with psalm assertion https://psalm.dev/docs/annotating_code/assertion_syntax/
Reported in vimeo/psalm#2191 (comment)
To reproduce:
/cc: @localheinz
The provider docblock line is set here, but $line
is actually the byte offset in the file, not the line number.
I have a phpunit test with data provider but I can't understand how to fix this errors:
MismatchingDocblockReturnType
1
MixedInferredReturnType
2
PossiblyInvalidArgument
3
class MyTest extends TestCase
{
/**
* @psalm-return list<array-key, array<array-key, mixed>> **1** **2**
*/
public function dataProvider(): array
{
return [
[ [], "message...'" ],
[ ['invalid' => true], "message" ],
[ ['foo' => 'value'], "message" ],
];
}
/**
* @test
* @dataProvider dataProvider
*/
public function my_test(array $payload, string $errorMessage): void **3**
{
}
}
Just wondering what the best approach is to using default arguments on a test method & documenting the return type with a data provider, as I'm currently getting TooFewArguments .... expecting at least 3, but saw 2 provided by .... ::dataprovider_markuparraytomarkupstring():(array<int, array{0:string, 1:array<array-key, mixed>}>)
, where the test method is something like:
public function test_thing(
string $expected,
array $source,
bool $as_xml = SomeOtherClassBecauseMutationTesting::DEFAULT_BOOL_AS_XML,
int $flags = Etc::DEFAULT_YOU_GET_THE_IDEA) {
...
}
and the data provider has an array return but only a couple of the inner arrays include some of the extra args?
I'm thinking typed args is better than shoving in a mixed variadic & when I tried doing @psalm-return array<int, array{0:string, 1:array, 2?:bool, 3?:int}>
it didn't exactly work as expected.
PHPUnit executes setUp()
and other @before
methods on each test run, so with regards to the test (test*()
and @test
) methods they behave more or less like a constructor.
When analyzing test classes, Psalm and the PHPUnit plugin report the properties initialized in setup methods as not initialized in constructor.
$ composer show | grep psalm
psalm/plugin-phpunit 0.15.1 Psalm plugin for PHPUnit
vimeo/psalm 4.5.2 A static analysis tool for find...
<?php
namespace Tests;
use PHPUnit\Framework\TestCase;
use stdClass;
class ObjectTest extends TestCase
{
/** @var stdClass */
private $object;
protected function setUp(): void
{
$this->object = new stdClass();
}
public function testObject(): void
{
self::assertIsNotNull($this->object);
}
}
$ psalm tests/ObjectTest.php
ERROR: PropertyNotSetInConstructor - tests/ObjectTest.php:11:13 - PropertyTests\ObjectTest::$object is not defined in constructor of Tests\ObjectTest and in any private or final methods called in the constructor (see https://psalm.dev/074)
private $object;
While it's technically true, it's irrelevant for the test cases. By the time when the test method is invoked, the property will be initialized.
It is possible to rework such tests to initialize the object explicitly for each test method but this is less convenient, more verbose and in fact invalidates the PHPUnit setup approach.
Data providers can be specified like this:
/**
* @dataProvider \Ergebnis\License\Test\Util\DataProvider\Text::blankOrEmptyString()
*
* @param string $name
*/
public function testFromFileRejectsBlankOrEmptyFileName(string $name): void
{
$this->expectException(Exception\InvalidFile::class);
Template::fromFile($name);
}
However, psalm/phpunit-psalm-plugin
complains with
ERROR: UndefinedMethod - test/Unit/HolderTest.php:33:21 - Provider method Ergebnis\License\Test\Unit\HolderTest::\Ergebnis\License\Test\Util\DataProvider\Text::blankOrEmptyString() is not defined
public function testFromStringRejectsBlankOrEmptyValue(string $value): void
ERROR: UndefinedMethod - test/Unit/TemplateTest.php:67:21 - Provider method Ergebnis\License\Test\Unit\TemplateTest::\Ergebnis\License\Test\Util\DataProvider\Text::blankOrEmptyString() is not defined
public function testFromStringRejectsBlankOrEmptyFileName(string $name): void
Is there a chance this can be supported as well?
For a concrete example, see ergebnis/license#23.
I had a provider with this signature:
public function providePropertyValue(): ?Generator
which although is not recommended is not incorrect either. The result was:
Uncaught Exception: should be iterable
Stack trace in the forked worker:
/path/to/vendor/psalm/plugin-phpunit/src/Hooks/TestCaseHandler.php
I think there is two issues here:
TestCaseHandler
which a simple message: "Unexpected: union has no way to get it constituent types", "should be iterable", "unexpected type": IMO it's not very user-friendly to throw those exceptions like without any more context... IMO requiring the user to have xdebug, disallow the xdebug handler (all of that is harder if using the PHAR too) and then debugging this just to be able to report the issue is making it a bit harder than necessary...Since PHPUnit has Psalm annotations now, should we deprecate this package for future versions of PHPUnit?
I am unsure whether this is right here. I am using psalm, phpunit and this plugin together. PHPUnit\Framework\MockObject\Builder\InvocationMocker
is marked as internal but it is the way to mock objects. So when I combine them I have to start mocking everything manually?
Since Symfony PHPUnit bridge installs PHPUnit in separate directory (not with other vendors) after installing Psalm's PHPUnit plugin I have 2 different versions of PHPUnit installed in the project (and my tests fail, which is different story..).
I would like Psalm to use already installed PHPUnit, with just proper configuration / bootstrap files.
How to achieve that?
InvalidArgument - tests/Feature/Cart/CartItems/GSuiteCartItemTest.php:87:122 - Argument 1 of Prophecy\Argument::that expects callable():bool, Closure(mixed):bool provided
The callable passed to Argument::that
is allowed to have parameters I believe. If I delete the stub entirely, my test passes however then we lose the benefit of the bool
typehint.
How do we inform psalm that any # of mixed params can be parameters of the callable?
I'm getting UndefinedMethod
and PossiblyUnusedMethod
false positives when I declare my data providers like this:
/** @dataProvider getTestCases */
Because the parser adds an extra space a the end of the method name.
This can be fixed by changing the following line:
-$apparent_provider_method_name = $class_storage->name . '::' . (string) $provider;
+$apparent_provider_method_name = $class_storage->name . '::' . trim((string) $provider);
But this doesn't fix the PossiblyUnusedMethod
error for that constructor.
Any ideas how to properly fix this?
PS: I wanted to send a PR with a test, but I can't get composer test
to run without errors on my machine before doing my changes. Is this a known issue (test failing) that I can ignore or am I doing something wrong?
It is unable to find Symfony\Component\Validator\Test\ForwardCompatTestTrait
540 / 796 (67%)
โโโโUncaught Exception: Could not locate trait statement
Stack trace in the forked worker:
#0 vendor/psalm/plugin-phpunit/hooks/TestCaseHandler.php(114): Psalm\Internal\Codebase\ClassLikes->getTraitNode('Symfony\\Compone...')
#1 vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(1062): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::afterStatementAnalysis(Object(PhpParser\Node\Stmt\Class_), Object(Psalm\Storage\ClassLikeStorage), Object(Psalm\Internal\Analyzer\ClassAnalyzer), Object(Psalm\Codebase), Array)
#2 vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(201): Psalm\Internal\Analyzer\ClassAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Context))
#3 vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(318): Psalm\Internal\Analyzer\FileAnalyzer->analyze(NULL)
#4 vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php(182): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(105, '')
#5 vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(435): Psalm\Internal\Fork\Pool->__construct(Array, Object(Closure), Object(Closure), Object(Closure), Object(Closure))
#6 vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(241): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 7)
#7 vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(528): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 7, false)
#8 vendor/vimeo/psalm/src/psalm.php(594): Psalm\Internal\Analyzer\ProjectAnalyzer->check('', false)
#9 vendor/vimeo/psalm/psalm(2): require_once('')
#10 {main} in vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php:339
Stack trace:
#0 vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php(371): Psalm\Internal\Fork\Pool->readResultsFromChildren()
#1 vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(444): Psalm\Internal\Fork\Pool->wait()
#2 vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(241): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 7)
#3 vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(528): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 7, false)
#4 vendor/vimeo/psalm/src/psalm.php(594): Psalm\Internal\Analyzer\ProjectAnalyzer->check('', false)
#5 vendor/vimeo/psalm/psalm(2): require_once('')
#6 {main}
(Psalm 3.9.4@352bd3f5c5789db04e4010856c2f4e01ed354f4e crashed due to an uncaught Throwable)
Versions
psalm/plugin-phpunit: 0.9.0
vimeo/psalm: 3.9.4
weirdan/doctrine-psalm-plugin: 0.10.0
The following error occours:
ERROR: MissingDependency - tests/myTest.php - PHPUnit\Framework\MockObject\Builder\InvocationMocker depends on class or interface phpunit\framework\mockobject\builder\parametersmatch that does not exist (see https://psalm.dev/157)
$aMock->expects($this->never())
The components from error
vendor/phpunit/phpunit/src/Framework/MockObject/Builder/
โโโ Identity.php
โโโ InvocationMocker.php
โโโ InvocationStubber.php
โโโ Match.php
โโโ MethodNameMatch.php
โโโ ParametersMatch.php
โโโ Stub.php
0 directories, 7 files
Psalm.xml config
<projectFiles>
<directory name="src"/>
<directory name="tests"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<plugins>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
</plugins>
Psalm 3.12.2@7c7ebd068f8acaba211d4a2c707c4ba90874fa26
"phpunit/phpunit": "^9.1.4"
"psalm/plugin-phpunit": "^0.10.1"
There's been quite a few changes since last release, could we have a new one?
The draft release is already there, please take a look.
Not sure if this is a psalm issue but thought I'd come here to clarify how stubs work.
Using PHPUnit 7 which has
/**
* Registers a new expectation in the mock object and returns the match
* object which can be infused with further details.
*
* @return InvocationMocker
*/
public function expects(Invocation $matcher);
in MockObject
.
This doesn't seem to appear in the stub but no error in IDE (possibly because psalm warning level is not high enough?)
When calling createMock(MyClass::class)
and deprecating __call
on MyClass
, I get errors about DeprecatedMethod
for uses of expects
.
Is that expected behaviour or is something incorrect?
Scenario: Stateful grandchild test case with setUp produces no MissingConstructor
Given I have the following code
"""
use Prophecy\Prophecy\ObjectProphecy;
class BaseTestCase extends TestCase {}
interface I { public function work(): int; }
class MyTestCase extends BaseTestCase
{
/** @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
Actual: MissingConstructor is reported on MyTestCase
As part of the upcoming changes in Psalm 4.x I'm removing support for @template-typeof
: vimeo/psalm#1732
0.5.1 is reporting TooFewArguments
in daft-nested-object, likely in reference to not being able to shove a variadic into an array shape on the Generator for the test
also getting issues with templated data providers, but that might be related to this initial issue.
When leveraging Psalm plugin, there is a weird case where upstream crashes.
See context : https://travis-ci.org/Roave/psr-container-doctrine/jobs/654211817
In my case, test case class has the following structure:
<?php
class MyTest extends \PHPUnit\Framework\TestCase
{
public function testOne(): string
{
// ...
return 'bar';
}
/**
* @dataProvider providerFoo
* @depends testOne
*/
public function testSecond(string $foo, string $bar): void
{
// ...
}
/**
* @return iterable<string, array{string}>
public function providerFoo(): iterable
{
return [['foo']];
}
}
In this case, testSecond()
method gets $foo = 'foo'
and $bar = 'bar'
arguments; but Psalm reports TooFewArguments
(probably ignoring dependency if data provider is set).
P.S.: Anyone please tell me how to suppress this bug until it gets fixed. Adding @psalm-suppress TooFewArguments
both in provider or test method docblocks doesn't help.
This is sort of similar to the issue I ran into with the Mockery plugin (vimeo/psalm#1537) in that a class_alias
is being used to conditionally alias one class to another. In this case, it's Zend Framework's zend-test
that's doing it:
https://github.com/zendframework/zend-test/blob/master/autoload/phpunit-class-aliases.php#L15
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
if (! class_exists(ExpectationFailedException::class)) {
class_alias(\PHPUnit_Framework_ExpectationFailedException::class, ExpectationFailedException::class);
}
if (! class_exists(TestCase::class)) {
class_alias(\PHPUnit_Framework_TestCase::class, TestCase::class);
}
That second if condition is causing Psalm, with this phpunit plugin enabled, to spit out this Fatal error:
Fatal error: Uncaught InvalidArgumentException: Could not get class storage for PHPUnit_Framework_TestCase in ./vendor/vimeo/psalm/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php:44
In my case \PHPUnit_Framework\TestCase
does not exist, as we're using PHPUnit 7.x which does not have that class defined. However, PHPUnit\Framework\TestCase
should exist, but perhaps it's not seen by Psalm when analyzing this autoloaded file?
Without the plugin enabled, the error is not thrown, but Psalm does complain about Zend's classes (which we extend) depending on that class (PHPUnit_Framework_TestCase
) and the class not existing.
If I comment out that second if condition in the file I link to above, all processes as expected.
Is this something that this plugin should deal with? Perhaps as an addition to the stubs?
don't have the repro on me at the moment (dayjob thing), but I forgot to add : array
onto one of my data providers earlier today & got a fatal error somewhere around here: https://github.com/psalm/phpunit-psalm-plugin/blob/e33ae73dc725acdf361645625667c65aa88731ee/hooks/TestCaseHandler.php#L180
With latest Psalm master one can just do
$codebase->methodExists($declaring_method_id, null, 'PHPUnit\Framework\TestSuite::run');
to reliably register a method as having been used, but looks like that doesn't work elsewhere
@muglug it seems Travis stopped reporting build status to github some time ago, even though the builds are actually performed. I don't have sufficient permissions on this repo to investigate - can you check if there's anything wrong with Travis integration?
How cumbersome would it be for this plugin to decorate the variables being asserted with appropriate flags?
i.e. TestCase::assertInstanceOf(class-string, $foo)
prior to Bar($foo)
where the method signature is Bar(WhateverTheClassStringWasInTheAssertion $obj)
and $foo
is of type WhateverTheClassStringWasInTheAssertion|null
results in Argument
of Bar cannot be null, possibly null value provided`
Similarly, when asserting that is_a()
returns true, psalm is not made aware that string $foo
is now class-string<SomeThing> $foo
, triggering InvalidStringClass
on invocation and TypeCoercion
when $foo
is later passed argument 1 to TestCase::assertInstanceOf()
:
function testThing(string $foo) : void {
static::assertTrue(is_a($foo, SomeThing::class, true));
$obj = new $type();
/* actual test stuff here */
}
Whilst running Psalm with the PHPUnit plugin on my project, at some point I caused it to break hard although I'm not sure from what exactly:
Full stack trace:
PHP Warning: assert(): assert(null !== $param->type) failed in /path/to/project/vendor/psalm/plugin-phpunit/src/Hooks/TestCaseHandler.php on line 397
Warning: assert(): assert(null !== $param->type) failed in /path/to/project/vendor/psalm/plugin-phpunit/src/Hooks/TestCaseHandler.php on line 397
โUncaught Exception: Argument 2 passed to Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::Psalm\PhpUnitPlugin\Hooks\{closure}() must be an instance of Psalm\Type\Union, null given, called in /path/to/project/vendor/psalm/plugin-phpunit/src/Hooks/TestCaseHandler.php on line 415
Stack trace in the forked worker:
#0 /path/to/project/vendor/psalm/plugin-phpunit/src/Hooks/TestCaseHandler.php(415): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::Psalm\PhpUnitPlugin\Hooks\{closure}(Object(Psalm\Type\Union), NULL, false, 0)
#1 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php(963): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::afterStatementAnalysis(Object(PhpParser\Node\Stmt\Class_), Object(Psalm\Storage\ClassLikeStorage), Object(Psalm\Internal\Analyzer\ClassAnalyzer), Object(Psalm\Codebase), Array)
#2 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php(211): Psalm\Internal\Analyzer\ClassAnalyzer->analyze(Object(Psalm\Context), Object(Psalm\Context))
#3 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(340): Psalm\Internal\Analyzer\FileAnalyzer->analyze(NULL)
#4 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php(193): Psalm\Internal\Codebase\Analyzer->Psalm\Internal\Codebase\{closure}(4, '/Users/tfidry/P...')
#5 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(406): Psalm\Internal\Fork\Pool->__construct(Array, Object(Closure), Object(Closure), Object(Closure), Object(Closure))
#6 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(269): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 3)
#7 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(639): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 3, false, true)
#8 /path/to/project/vendor/vimeo/psalm/src/psalm.php(676): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/Users/tfidry/P...', true)
#9 /path/to/project/vendor/vimeo/psalm/psalm(2): require_once('/Users/tfidry/P...')
#10 {main} in /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php:357
Stack trace:
#0 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Fork/Pool.php(389): Psalm\Internal\Fork\Pool->readResultsFromChildren()
#1 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(473): Psalm\Internal\Fork\Pool->wait()
#2 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Codebase/Analyzer.php(269): Psalm\Internal\Codebase\Analyzer->doAnalysis(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 3)
#3 /path/to/project/vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php(639): Psalm\Internal\Codebase\Analyzer->analyzeFiles(Object(Psalm\Internal\Analyzer\ProjectAnalyzer), 3, false, true)
#4 /path/to/project/vendor/vimeo/psalm/src/psalm.php(676): Psalm\Internal\Analyzer\ProjectAnalyzer->check('/Users/tfidry/P...', true)
#5 /path/to/project/vendor/vimeo/psalm/psalm(2): require_once('/Users/tfidry/P...')
#6 {main}
(Psalm 4.3.1@2feba22a005a18bf31d4c7b9bdb9252c73897476 crashed due to an uncaught Throwable)
make: *** [psalm] Error 1
PHPUnit 9.4.4 & Psalm 4.3.1
using:
daft-markup\Tests\ValidatorTest.php:295
daft-markup\Tests\ValidatorTest.php:297
PHP Warning: assert(): assert(null !== $param->type) failed in daft-markup\vendor\psalm\plugin-phpunit\hooks\TestCaseHandler.php on line 351
PHP Fatal error: Uncaught TypeError: Argument 2 passed to Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::Psalm\PhpUn
Warning: assert(): assert(null !== $param->type) failed in daft-markup\vendor\psalm\plugin-phpunit\hooks\TestCaseHandler.php on line 351
itPlugin\Hooks{closure}() must be an instance of Psalm\Type\Union, null given, called in daft-markup\vendor\psalm\plugin-phpunit\hooks\TestCaseHandler.php on line 370 and defined in daft-markup\vendor\psalm\plugin-phpunit\hooks\TestCaseHandler.php:263Stack trace:
- #โ0 daft-markup\vendor\psalm\plugin-phpunit\hooks\TestCaseHandler.php(370): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::Psalm\PhpUnitPlugin\Hooks{closure}(Object(Psalm\Type\Union), NULL, false, 0)
- #โ1 daft-markup\vendor\vimeo\psalm\src\Psalm\Internal\Analyzer\ClassAnalyzer.php(942): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::afterStatementAnalysis(Object(PhpParser\Node\Stmt\Class_), Object(Psalm\Storage\ClassLikeStorage), Object(Psalm\Internal\Analyzer\NamespaceAnalyzer), Object(Psalm\Codebase), Array)
- #โ2 daft-markup\vendor\vimeo\psalm\src\Psal in daft-markup\vendor\psalm\plugin-phpunit\hooks\TestCaseHandler.php on line 263
Fatal error: Uncaught TypeError: Argument 2 passed to Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::Psalm\PhpUnitPlugin\Hooks{closure}() must be an instance of Psalm\Type\Union, null given, called in daft-markup\vendor\psalm\plugin-phpunit\hooks\TestCaseHandler.php on line 370 and defined in daft-markup\vendor\psalm\plugin-phpunit\hooks\TestCaseHandler.php:263
Stack trace:
- #โ0 daft-markup\vendor\psalm\plugin-phpunit\hooks\TestCaseHandler.php(370): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::Psalm\PhpUnitPlugin\Hooks{closure}(Object(Psalm\Type\Union), NULL, false, 0)
- #โ1 daft-markup\vendor\vimeo\psalm\src\Psalm\Internal\Analyzer\ClassAnalyzer.php(942): Psalm\PhpUnitPlugin\Hooks\TestCaseHandler::afterStatementAnalysis(Object(PhpParser\Node\Stmt\Class_), Object(Psalm\Storage\ClassLikeStorage), Object(Psalm\Internal\Analyzer\NamespaceAnalyzer), Object(Psalm\Codebase), Array)
- #โ2 daft-markup\vendor\vimeo\psalm\src\Psal in daft-markup\vendor\psalm\plugin-phpunit\hooks\TestCaseHandler.php on line 263
/**
* @param mixed $markupContent
* @param class-string<\Throwable> $expected_message
*
* @dataProvider dataProvider_ValidateContent_failure
*/
public function test_ValidateContent_failure(
$markup_content,
string $expected_exception,
string $expected_message
) : void {
static::expectException($expected_exception);
static::expectExceptionMessage($expected_message);
MarkupValidator::ValidateContent($markup_content);
}
/**
* @param mixed $markupContent
*/
final public static function ValidateContent($markupContent) : void
{
if ( ! is_array($markupContent)) {
throw new InvalidArgumentException('Element content must be specified as an array!');
}
/**
* @var array<int|string, mixed>
*/
$markupContent = $markupContent;
foreach (array_keys($markupContent) as $key) {
if ( ! is_scalar($markupContent[$key]) && ! is_array($markupContent[$key])) {
throw new InvalidArgumentException('Element content must be scalar or an array!');
}
}
}
Found that issue while working on doctrine/sql-formatter#51 (comment)
With the plugin loaded in psalm.xml
:
vendor/bin/psalm --output-format=checkstyle
Scanning files...
Analyzing files...
โโโโโโโโโโโโโ
Without:
Scanning files...
Analyzing files...
โโโโโโโโโโโโโ
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle>
</checkstyle>
Why is there no checkstyle output in the former?
In PHPUnit 7.4 (probably all 7.x series) this returns PHPUnit\Framework\MockObject\Builder\InvocationMocker
, not static
. What PHPUnit version you were basing stubs on?
TestCaseHander::unionizeIterables()
seems to choke on Psalm\Type\Atomic\TList
presently at the dayjob, going to try and PR from the web interface. had to make two attempts to get a nice diff in the web interface: #42
Scenario: Descendant of a test that has setUp produces no MissingConstructor
Given I have the following code
"""
use Prophecy\Prophecy\ObjectProphecy;
interface I { public function work(): int; }
class BaseTestCase extends TestCase {
/** @var ObjectProphecy<I> */
protected $i;
/** @return void */
public function setUp() {
$this->i = $this->prophesize(I::class);
}
}
class MyTestCase extends BaseTestCase
{
/** @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
Actual:
Type | Message |
---|---|
MissingConstructor | NS\MyTestCase has an uninitialized variable $this->i, but no constructor |
What do you think about skipping analysys for skipped test? (or marked as incomplete?)
Currently the plugin does not validate test methods and providers in traits, that are used in test cases.
Example:
Scenario: Test methods and providers in trait used by a test case are validated
Given I have the following code
"""
trait MyTestTrait {
/**
* @return void
* @dataProvider provide
*/
public function testSomething(string $_p) {}
/**
* @return iterable<int,int[]>
*/
public function provide() { return [[1]]; }
}
class MyTestCase extends TestCase {
use MyTestTrait;
}
"""
When I run Psalm
Then I see these errors
| Type | Message |
| InvalidArgument | Argument 1 of NS\MyTestTrait::testSomething expects string, int provided by NS\MyTestTrait::provide():(iterable<int, array<array-key, int>>) |
And I see no other errors
Actual: no issues reported
On Psalm latest
./psalm --clear-cache
git checkout b3f6b56
./psalm --threads=8 --diff --diff-methods --show-info=false
./psalm --threads=8 --diff --diff-methods --show-info=false
git checkout 6f6e265
./psalm --threads=8 --diff --diff-methods --show-info=false
Expected: No issues
Actual: A whole bunch of PossiblyUnusedMethod
issues that would normally be covered by the PHPUnit plugin
This is probably a bug in Psalm, rather than the plugin, but storing here because it affects the plugin.
I've been using Gherkin tests for some time on weirdan/doctrine-psalm-plugin
and I find it extremely helpful so far. @muglug , would you think phpunit-psalm-plugin
could benefit from it as well? I'm willing to contribute it.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.