PHPackages                             mrpunyapal/peststan - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. mrpunyapal/peststan

ActivePhpstan-extension[Testing &amp; Quality](/categories/testing)

mrpunyapal/peststan
===================

PHPStan extension for Pest PHP testing framework

0.2.10(1mo ago)4983.7k↑20609.5%3[1 issues](https://github.com/MrPunyapal/PestStan/issues)17MITPHPPHP ^8.2CI passing

Since Dec 28Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/MrPunyapal/PestStan)[ Packagist](https://packagist.org/packages/mrpunyapal/peststan)[ GitHub Sponsors](https://github.com/mrpunyapal)[ RSS](/packages/mrpunyapal-peststan/feed)WikiDiscussions main Synced 3d ago

READMEChangelog (10)Dependencies (21)Versions (21)Used By (17)

PestStan
========

[](#peststan)

[![Latest Version on Packagist](https://camo.githubusercontent.com/0e091f39f03933f2efdfa6805dbc0cf7d3909ad72a3ae318972e4557810ffb1b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d7270756e796170616c2f706573747374616e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/mrpunyapal/peststan)[![Total Downloads](https://camo.githubusercontent.com/f5dc85c0b768b59db3f2328387a868f7ab00f5798494f2a3cb605e250cfca613/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6d7270756e796170616c2f706573747374616e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/mrpunyapal/peststan)[![CI](https://github.com/mrpunyapal/peststan/actions/workflows/ci.yml/badge.svg)](https://github.com/mrpunyapal/peststan/actions/workflows/ci.yml)

PHPStan extension for [Pest PHP](https://pestphp.com/) testing framework. Provides type-safe expectations, proper `$this` binding in test closures, and accurate return types for all Pest functions.

Requirements
------------

[](#requirements)

- PHP ^8.2
- PHPStan ^2.0
- Pest PHP ^3.0, ^4.0, or ^5.0

Installation
------------

[](#installation)

```
composer require --dev mrpunyapal/peststan
```

If you have [phpstan/extension-installer](https://github.com/phpstan/extension-installer) (recommended), the extension is registered automatically.

Otherwise, add it manually to your `phpstan.neon` or `phpstan.neon.dist`:

```
includes:
    - vendor/mrpunyapal/peststan/extension.neon
```

Features
--------

[](#features)

### Generic `expect()` Function

[](#generic-expect-function)

The extension provides generic type inference for Pest's `expect()` function, so PHPStan knows the exact type of the expectation value:

```
expect('hello');           // Expectation
expect(42);                // Expectation
expect(['a' => 1]);        // Expectation
expect($user);             // Expectation
expect();                  // Expectation
```

### Type Narrowing Assertions

[](#type-narrowing-assertions)

Type-checking assertion methods narrow the generic type parameter, so PHPStan tracks the type through assertion chains:

```
/** @var int|string $value */
$value = getValue();

expect($value)->toBeString();
// PHPStan now knows the expectation wraps a string

expect($value)->toBeInstanceOf(User::class);
// PHPStan now knows the expectation wraps a User
```

Supported type-narrowing assertions: `toBeString`, `toBeInt`, `toBeFloat`, `toBeBool`, `toBeArray`, `toBeList`, `toBeObject`, `toBeCallable`, `toBeIterable`, `toBeNumeric`, `toBeScalar`, `toBeResource`, `toBeTrue`, `toBeFalse`, `toBeNull`, `toBeInstanceOf`.

### Type-Safe `and()` Chaining

[](#type-safe-and-chaining)

The `and()` method properly changes the generic type parameter, enabling type-safe assertion chains:

```
expect('hello')
    ->toBeString()       // Expectation
    ->and(42)            // Expectation
    ->toBeInt()          // Expectation
    ->and(['a', 'b'])    // Expectation
    ->toHaveCount(2);    // Expectation
```

### `$this` Binding in Test Closures

[](#this-binding-in-test-closures)

The extension ensures `$this` is properly typed inside all Pest test closures and lifecycle hooks. It auto-detects your TestCase class from your `Pest.php` configuration file:

```
// tests/Pest.php
uses(Tests\TestCase::class)->in('Feature');

// tests/Feature/ExampleTest.php
it('can access test case methods', function () {
    $this->get('/');  // PHPStan knows $this is Tests\TestCase
});

beforeEach(function () {
    $this->assertTrue(true);   // Works in hooks too
});
```

Supported functions: `it()`, `test()`, `describe()`, `beforeEach()`, `afterEach()`, `beforeAll()`, `afterAll()`.

### Dynamic Properties in Test Closures

[](#dynamic-properties-in-test-closures)

Pest allows setting properties on `$this` inside `beforeEach` hooks. The extension reads those assignments and **infers the exact type** — no `@var` annotation or extra local variable required:

```
beforeEach(function () {
    $this->post   = new Post;                    // Post
    $this->title  = 'Hello';                     // 'Hello' (constant string)
    $this->count  = 42;                          // 42 (constant int)
    $this->active = true;                        // true
});

it('knows the property types', function () {
    $this->post->title;          // PHPStan knows $this->post is Post — no "Cannot access property on mixed" error
    strlen($this->title);        // fine — PHPStan knows it is a string
});
```

For method-call chains such as factory calls, annotate the local variable with `@var` to guide inference:

```
beforeEach(function () {
    /** @var User $user */
    $user        = User::factory()->create();
    $this->user  = $user;        // User
});
```

If the same property is set by multiple hooks the type is **unioned**:

```
beforeEach(function () { $this->item = new Post; });
beforeEach(function () { $this->item = new Comment; });

it('sees the union', function () {
    $this->item;  // Post|Comment
});
```

Properties that are never set in a hook remain `mixed`.

Configuration
-------------

[](#configuration)

### Automatic TestCase Detection

[](#automatic-testcase-detection)

PestStan reads your `Pest.php` files to determine which TestCase class is used in each test directory. It supports the `uses()` pattern:

```
// uses(TestCase::class)->in('Feature');
```

No configuration needed — it discovers `Pest.php` files automatically from your PHPStan `paths`.

### Manual TestCase Override

[](#manual-testcase-override)

If auto-detection doesn't work for your setup, or you want a global default, set it in your `phpstan.neon`:

```
parameters:
    peststan:
        testCaseClass: App\Testing\TestCase
```

### Explicit Pest.php Paths

[](#explicit-pestphp-paths)

If your `Pest.php` files aren't within PHPStan's analysis paths, you can specify them explicitly:

```
parameters:
    peststan:
        pestConfigFiles:
            - tests/Pest.php
```

### Pest Function Return Types

[](#pest-function-return-types)

Accurate return types for Pest's core global functions, plus newer helpers when they exist in the installed Pest version:

FunctionReturn Type`expect($value)``Expectation``pest()``Configuration``uses(...)``UsesCall``it()` / `test()` / `todo()``TestCall``describe()``DescribeCall``beforeEach()``BeforeEachCall``afterEach()``AfterEachCall``fixture()``string` when available`beforeAll()` / `afterAll()``null``dataset()` / `covers()` / `mutates()``null`### `not()` and `each()` Return Types

[](#not-and-each-return-types)

```
expect('hello')->not();    // OppositeExpectation
expect([1, 2])->each();    // EachExpectation
```

### TestCall Chaining

[](#testcall-chaining)

All `TestCall` methods are properly typed for fluent chaining:

```
it('does something', function () { /* ... */ })
    ->with(['a', 'b'])
    ->group('unit', 'feature')
    ->skip(false)
    ->depends('other test')
    ->throws(RuntimeException::class)
    ->repeat(3);
```

When you set `peststan.testCaseClass` to a custom class, PestStan also exposes that class's public helper methods on `TestCall` chains:

```
it('uses a custom helper')->publicHelper();
```

### Architecture Testing Support

[](#architecture-testing-support)

Architecture testing methods are fully supported:

```
expect('App\Models')
    ->toExtend('Illuminate\Database\Eloquent\Model')
    ->ignoring('App\Models\Legacy');

expect('App')
    ->classes()
    ->toBeFinal();

expect('App\Actions')->toBeInvokable();
expect('App\DTOs')->toBeReadonly();
expect('App')->toUseStrictTypes();
```

Static Analysis Rules
---------------------

[](#static-analysis-rules)

PestStan ships with rules that catch common Pest mistakes at static analysis time, before your tests run.

### `pest.test.emptyClosure` — Empty test body

[](#pesttestemptyclosure--empty-test-body)

Detects tests whose closure contains no statements.

```
it('does something'); // fine — todo test
it('does something', function () {});
// ✘ Test 'does something' has an empty closure body. Did you forget to add assertions?

```

### `pest.test.staticClosure` — Static test closure

[](#pestteststaticclosure--static-test-closure)

Pest binds `$this` inside every test closure to the `TestCase` instance. Marking the closure `static` prevents that binding.

```
it('example', static function () {
// ✘ Test closure passed to it() must not be static.
    expect(true)->toBeTrue();
});
```

### `pest.lifecycle.beforeAllDisallowed` / `pest.lifecycle.afterAllDisallowed` — Lifecycle hooks inside `describe()`

[](#pestlifecyclebeforealldisallowed--pestlifecycleafteralldisallowed--lifecycle-hooks-inside-describe)

Pest does not support `beforeAll()` or `afterAll()` inside `describe()` blocks — calling them throws at runtime.

```
describe('suite', function () {
    beforeAll(function () { /* ... */ });
    // ✘ beforeAll() cannot be used inside describe() blocks.

    afterAll(function () { /* ... */ });
    // ✘ afterAll() cannot be used inside describe() blocks.
});
```

### `pest.execution.invalidRepeatValue` — Invalid `repeat()` count

[](#pestexecutioninvalidrepeatvalue--invalid-repeat-count)

`repeat()` requires a positive integer greater than zero.

```
it('runs multiple times', function () { /* ... */ })->repeat(0);
// ✘ repeat() requires a value greater than 0, got 0.
```

### `pest.test.duplicateDescription` — Duplicate test description

[](#pesttestduplicatedescription--duplicate-test-description)

Two tests in the same file with the same description will collide at runtime.

```
it('does something', fn () => expect(1)->toBe(1));
it('does something', fn () => expect(2)->toBe(2));
// ✘ A test with the description 'it does something' already exists in this file.
```

### `pest.expectation.impossible` — Assertion that always fails

[](#pestexpectationimpossible--assertion-that-always-fails)

When the static type already makes an assertion impossible, PestStan reports it.

```
expect(42)->toBeString();
// ✘ Calling toBeString() on Expectation; assertion is impossible.

expect('hello')->toBeNull();
// ✘ Calling toBeNull() on Expectation; assertion is impossible.
```

Covered assertions: `toBeString`, `toBeInt`, `toBeFloat`, `toBeBool`, `toBeTrue`, `toBeFalse`, `toBeNull`, `toBeArray`, `toBeList`, `toBeObject`, `toBeCallable`, `toBeIterable`, `toBeNumeric`, `toBeScalar`, `toBeInstanceOf`.

### `pest.expectation.redundant` — Assertion that always passes

[](#pestexpectationredundant--assertion-that-always-passes)

When the static type already guarantees an assertion will always succeed, the assertion is redundant and adds no value.

```
expect(true)->toBeTrue();
// ✘ Calling toBeTrue() on Expectation; assertion is redundant.

expect('hello')->toBeString();
// ✘ Calling toBeString() on Expectation; assertion is redundant.

expect(42)->toBeNumeric();
// ✘ Calling toBeNumeric() on Expectation; assertion is redundant.
```

Covered assertions: `toBeString`, `toBeInt`, `toBeFloat`, `toBeBool`, `toBeTrue`, `toBeFalse`, `toBeNull`, `toBeArray`, `toBeList`, `toBeObject`, `toBeCallable`, `toBeIterable`, `toBeNumeric`, `toBeScalar`, `toBeInstanceOf`.

### `pest.expectation.requiresIterable` / `pest.expectation.requiresString` — Incompatible value type

[](#pestexpectationrequiresiterable--pestexpectationrequiresstring--incompatible-value-type)

Some expectation methods require the value to satisfy a pre-condition.

```
expect(42)->each(fn ($e) => $e->toBeInt());
// ✘ Calling each() on Expectation; matcher requires iterable.

expect(42)->toBeJson();
// ✘ Calling toBeJson() on Expectation; matcher requires string.
```

Methods requiring an iterable: `each`, `sequence`.
Methods requiring a string: `json`, `toStartWith`, `toEndWith`, `toBeJson`, `toBeDirectory`, `toBeFile`, `toBeReadableFile`, `toBeWritableFile`, `toBeReadableDirectory`, `toBeWritableDirectory`.

### `pest.lifecycle.beforeAllThisUsage` — `$this` inside `beforeAll()`

[](#pestlifecyclebeforeallthisusage--this-inside-beforeall)

`beforeAll()` runs once in a static context before any tests in the file. `$this` is not available.

```
beforeAll(function () {
    $this->db = new Database; // ✘ beforeAll() runs in static context — $this is not available. Use beforeEach() instead.
});
```

Use `beforeEach()` to run setup before each test with `$this` available.

### `pest.throws.classNotFound` / `pest.throws.invalidException` — Invalid `throws()` argument

[](#pestthrowsclassnotfound--pestthrowsinvalidexception--invalid-throws-argument)

`throws()` accepts a class name that implements `Throwable`. Passing a non-existent class or a class that is not `Throwable` is caught at analysis time.

```
it('fails', function () { ... })->throws('App\NonExistentException');
// ✘ Class App\NonExistentException passed to throws() does not exist.

it('fails', function () { ... })->throws(stdClass::class);
// ✘ throws() expects a Throwable class, got stdClass.
```

### `pest.covers.classNotFound` / `pest.covers.functionNotFound` — Non-existent symbol in `coversClass()`

[](#pestcoversclassnotfound--pestcoversfunctionnotfound--non-existent-symbol-in-coversclass)

`coversClass()`, `coversTrait()`, and `coversFunction()` reference symbols by name. PestStan verifies those symbols exist.

```
it('covers something', function () { ... })->coversClass('App\Nonexistent\Service');
// ✘ Class App\Nonexistent\Service referenced in coversClass() does not exist.
```

### `pest.describe.withoutTests` — Empty `describe()` block

[](#pestdescribewithouttests--empty-describe-block)

A `describe()` block that contains no `it()` or `test()` calls (only hooks, or nothing at all) is likely a mistake.

```
describe('UserService', function () {
    beforeEach(fn () => null);
    // ✘ describe() block 'UserService' contains no tests.
});
```

### `pest.group.invalidName` — Empty `group()` name

[](#pestgroupinvalidname--empty-group-name)

`group()` requires at least one non-empty, non-whitespace string argument.

```
it('example', fn () => null)->group('');
// ✘ group() requires a non-empty string argument.
```

### Ignoring rules

[](#ignoring-rules)

All rules use PHPStan [identifiers](https://phpstan.org/user-guide/ignoring-errors), so you can suppress them selectively in your baseline or inline:

```
# phpstan.neon
parameters:
    ignoreErrors:
        - identifier: pest.test.emptyClosure
```

```
/** @phpstan-ignore pest.test.staticClosure */
it('example', static fn () => expect(true)->toBeTrue());
```

Canonical identifiers are emitted in diagnostics and rule errors. Legacy identifiers remain resolvable through [src/Diagnostics/PestDiagnosticIdentifiers.php](src/Diagnostics/PestDiagnosticIdentifiers.php) so downstream tooling can canonicalize older baselines or stored metadata safely.

Semantic Architecture
---------------------

[](#semantic-architecture)

PestStan stays on the analysis side of the ecosystem boundary. [src/Analysis/Expectation/ExpectationSemanticAnalyzer.php](src/Analysis/Expectation/ExpectationSemanticAnalyzer.php) owns chain-aware reasoning, [src/Analysis/Expectation/ExpectationChainStateResolver.php](src/Analysis/Expectation/ExpectationChainStateResolver.php) propagates expectation state through fluent chains, and [src/Analysis/Expectation/ExpectationTypeNarrower.php](src/Analysis/Expectation/ExpectationTypeNarrower.php) applies conservative type narrowing without speculative inference.

Diagnostics are the interoperability contract surface. [src/Diagnostics/PestDiagnostic.php](src/Diagnostics/PestDiagnostic.php) is immutable and JSON-safe, [src/Diagnostics/PestDiagnostics.php](src/Diagnostics/PestDiagnostics.php) emits canonical identifiers plus machine-readable metadata, and [src/Diagnostics/PestDiagnosticIdentifiers.php](src/Diagnostics/PestDiagnosticIdentifiers.php) resolves legacy aliases to the canonical taxonomy.

The boundary with `rector-pest` is intentional: PestStan reports stable semantic facts, while Rector consumes those facts to decide whether an automated remediation is safe. PestStan does not apply fixes; it provides deterministic identifiers, semantic codes, matcher categories, and type reasoning that other tools can consume.

Testing
-------

[](#testing)

```
composer test        # Run all checks (lint + types + unit)
composer lint        # Apply code style fixes (Rector + Pint)
composer test:lint   # Check code style (dry-run)
composer test:types  # Run PHPStan analysis
composer test:unit   # Run Pest unit tests
```

Contributing
------------

[](#contributing)

Contributions are welcome! Please feel free to submit a Pull Request.

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for more information.

Credits
-------

[](#credits)

- Built for [Pest PHP](https://pestphp.com/)
- Powered by [PHPStan](https://phpstan.org/)

###  Health Score

53

—

FairBetter than 96% of packages

Maintenance89

Actively maintained with recent releases

Popularity45

Moderate usage in the ecosystem

Community23

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 98.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~7 days

Total

20

Last Release

55d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/230c58a4f918ca3e3f2988b38721230698bce88f76ae9087e4377ba0b3a074d5?d=identicon)[MrPunyapal](/maintainers/MrPunyapal)

---

Top Contributors

[![MrPunyapal](https://avatars.githubusercontent.com/u/53343069?v=4)](https://github.com/MrPunyapal "MrPunyapal (167 commits)")[![nunomaduro](https://avatars.githubusercontent.com/u/5457236?v=4)](https://github.com/nunomaduro "nunomaduro (1 commits)")[![raphaelstolt](https://avatars.githubusercontent.com/u/48225?v=4)](https://github.com/raphaelstolt "raphaelstolt (1 commits)")

---

Tags

pestphpphpstanphpstan-extensiontestingpestPHPStanstatic analysis

###  Code Quality

TestsPest

Static AnalysisRector

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/mrpunyapal-peststan/health.svg)

```
[![Health](https://phpackages.com/badges/mrpunyapal-peststan/health.svg)](https://phpackages.com/packages/mrpunyapal-peststan)
```

###  Alternatives

[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.5k55.4M8.5k](/packages/larastan-larastan)[phpstan/phpstan-symfony

Symfony Framework extensions and rules for PHPStan

79475.7M2.2k](/packages/phpstan-phpstan-symfony)[shipmonk/dead-code-detector

Dead code detector to find unused PHP code via PHPStan extension. Can automatically remove dead PHP code. Supports libraries like Symfony, Doctrine, PHPUnit etc. Detects dead cycles. Can detect dead code that is tested.

4853.5M92](/packages/shipmonk-dead-code-detector)[phpstan/phpstan-doctrine

Doctrine extensions for PHPStan

67272.8M1.4k](/packages/phpstan-phpstan-doctrine)[ekino/phpstan-banned-code

Detected banned code using PHPStan

3016.2M119](/packages/ekino-phpstan-banned-code)[staabm/phpstan-dba

2942.6M2](/packages/staabm-phpstan-dba)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
