mrpunyapal/peststan
PHPStan extension for Pest PHP that adds type-safe expectations, proper $this binding in test closures, generic expect() inference, type-narrowing assertions, and accurate return types for all Pest functions. Requires PHP 8.2+, PHPStan 2, Pest 3-5.
Architecture fit
phpstan/extension-installer (or an explicit includes: vendor/mrpunyapal/peststan/extension.neon). It adds no runtime code, no service provider, no config publish step — so it sits entirely in the dev/CI layer and cannot affect production behavior.expect(), it(), test(), describe(), hooks, dataset(), covers(), etc.), (b) narrowing the Expectation<T> generic through assertion chains, (c) inferring $this binding in closures by reading uses(TestCase::class)->in('...') out of tests/Pest.php, and (d) registering ~15 custom rules with stable identifiers (pest.test.emptyClosure, pest.expectation.impossible, pest.lifecycle.beforeAllThisUsage, …).Integration feasibility
composer require --dev, auto-registration, optional parameters.peststan.* block. A single engineer can validate in a spike branch in a few hours.$this in beforeAll). That is a cleanup wave, not a config change.Technical risk
Archived: yes. A package with zero dependents and 53 stars has no community safety net, and archiving means no future compatibility work. PHPStan custom rules and type-narrowing extensions lean on semi-internal APIs that change across majors; the moment we move to PHPStan 3.x or Pest 6.x, this extension is the most likely thing to break and nobody upstream will fix it.Pest.php TestCase auto-detection is heuristic and path-scoped. In monorepos or multi-root test layouts it can quietly produce mixed or unbound $this, which reads to developers as "the extension is doing nothing" rather than an error.pest.expectation.redundant and pest.test.staticClosure are opinionated; the cheap escape hatch is a blanket ignoreErrors entry, which permanently guts the value of the rule.Key questions
uses() detection actually resolve for our test tree (multiple Pest.php files, tests/Unit, tests/Feature, modules with their own test roots)?phpstan.neon and baseline diff, and what is our policy on @phpstan-ignore for these identifiers?beforeEach a real win for us, or do our suites already annotate with @var?Stack fit
require-dev only; confirm it is excluded from production autoload/--no-dev deploys. Nothing else in the app changes.Migration path
main, and quantify it (count by identifier, by directory). This is the go/no-go number.beforeAll $this) are inexpensive to reimplement in-house; the generic Expectation<T> narrowing is not.Compatibility
fixture() "when available") implies behavior varies by installed Pest version, which is a compatibility surface worth testing.parameters.peststan.pestConfigFiles explicitly rather than relying on path discovery if the test tree is non-standard.Sequencing
Maintenance
Support
Scaling
beforeEach/beforeAll closures (dynamic property inference walks those bodies). On large suites this is measurable but usually small relative to PHPStan's existing analysis time — benchmark before/after rather than assuming.$this usage gets real value; a 50-test suite with three files does not justify the ownership burden.Failure modes
pest.* wildcards.Pest.php detection failure: $this is mixed and dynamic properties are untyped — looks like the tool "isn't working." Mitigation: set testCaseClass explicitly and document it.phpstan.neon.dist inherited by all apps.Ramp-up
How can I help you explore Laravel packages today?