alies-dev / psalm-plugin-pest
Psalm plugin for Pest: types $this in test closures as the configured TestCase and accepts Pest's public API.
Package info
github.com/alies-dev/psalm-plugin-pest
Type:psalm-plugin
pkg:composer/alies-dev/psalm-plugin-pest
Requires
- php: ^8.3
- nikic/php-parser: ^5.0
- vimeo/psalm: ^6.19 || ^7.0
Requires (Dev)
- alies-dev/psalm-tester: ^0.4
- friendsofphp/php-cs-fixer: ^3.94
- pestphp/pest: ^4.0
- phpunit/phpunit: ^12.5
- rector/rector: ^2.5
- symfony/filesystem: ^7.4 || ^8.0
- symfony/process: ^7.4 || ^8.0
Suggests
- pestphp/pest: The test framework this plugin models. Without it installed the plugin is inert.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-30 16:51:22 UTC
README
Makes Psalm understand Pest test files.
$thisin test closures. Pest binds everytest(),it(),beforeEach()andafterEach()closure to a TestCase, so$this->useror$this->actingAs()is valid code. Pest documents that binding only as@param-closure-this TestCall, which Psalm 6.19+ reads literally: every$this->...in a test becomesUndefinedThisPropertyFetchorUndefinedMethodonTestCall, plus aMixed*cascade. The plugin binds$thisto the class Pest actually uses.- Pest's public API. Pest marks the classes behind
expect()->toBe(),uses()->in(),pest()->extend()andtest()->group()as@internal. The plugin stops reportingInternalMethodon them. Pest classes outside that API keep reporting.
Installation
composer require --dev alies-dev/psalm-plugin-pest
vendor/bin/psalm-plugin enable alies-dev/psalm-plugin-pest
Requirements: PHP 8.3+ (Pest 4's floor), Psalm 6.19+ or 7, Pest 4. Your test directory must be part of <projectFiles>.
How the TestCase is resolved
For each test file, first match wins:
- The class passed to
uses(...)orpest()->extend(...)in the test file itself. - The class assigned to the file's directory through
uses(...)->in(...)orpest()->extend(...)->in(...)intests/Pest.php, or in any other file Pest loads at boot:tests/Helpers.php,tests/Expectations.php, thetests/Helpers/andtests/Expectations/trees, everyDatasets.php, and every file under aDatasets/directory.in()accepts string literals, globs,__DIR__and__DIR__ . '/Feature'. PHPUnit\Framework\TestCase, Pest's default, for files insidetests/.
Traits passed to uses() never decide the class. The files are parsed, never executed.
When the answer is uncertain the plugin keeps Pest's own TestCall binding rather than guess.
Known limitations
tests/Pest.phpis looked up next to your Psalm config. A custom test directory (Pest's--test-directory) is not detected, and its tests keep Pest's binding.- A test file outside
tests/(for example a monorepo package with its own suite) keeps Pest's binding. - A boot file the plugin cannot read statically disables resolution for the whole suite. That covers a variable argument, a call inside a condition or a closure, an
include, a top-levelreturnorexit, an aliaseduse function uses, and a symlinked file. uses()->in()written in one test file to configure other files is ignored.- Analyzing a single test file on its own declines when its TestCase class was not scanned; full project runs are unaffected.
- Properties a test assigns dynamically (
$this->user = ...inbeforeEach()) are reported, because Psalm now sees the real TestCase. Declare them on the TestCase. - Psalm 7.0.0-beta22 has no
@param-closure-thissupport, so there$thisin a test closure is stillInvalidScope. Binding activates automatically on a Psalm 7 release that carries it (vimeo/psalm master does). TheInternalMethodrelief works on every supported version. - Pest 5 is untested. The plugin keys on Pest's
@param-closure-this TestCalltag and class names, and does nothing if those change.
License
MIT, see LICENSE.