rasuvaeff/understudy-psalm

Psalm plugin for the understudy test double library: matcher-aware setup closures and misuse diagnostics

Maintainers

Package info

github.com/rasuvaeff/understudy-psalm

Type:psalm-plugin

pkg:composer/rasuvaeff/understudy-psalm

Transparency log

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.3 2026-08-27 17:55 UTC

This package is auto-updated.

Last update: 2026-08-27 17:55:33 UTC


README

Latest Stable Version Total Downloads Build Static analysis Psalm level PHP License Русская версия

Psalm plugin for understudy.

Using an AI coding assistant? Point it at llms.txt.

Requirements

  • PHP 8.3 - 8.5
  • vimeo/psalm ^6.16
  • rasuvaeff/understudy ^0.1

Installation

composer require --dev rasuvaeff/understudy-psalm
vendor/bin/psalm-plugin enable rasuvaeff/understudy-psalm

What it does

understudy specifies a call by making it inside a closure:

when(fn () => $repo->find(Arg::int(min: 1)))->returns($book);

Arg::int() is declared mixed, because a matcher has to be passable wherever the contract declares anything at all. Psalm reports that as an argument-type error — correctly, in general, and wrongly here.

The plugin drops that report inside a specification closure, and only there:

Where Psalm
when(fn () => $repo->find(Arg::int())) silent
Understudy::expect(fn () => $repo->find(Arg::any())) silent
Understudy::calls(fn () => $repo->find(Arg::any())) silent
Understudy::verifySequence(fn () => ..., fn () => ...) silent
$repo->find(Arg::int()) — a real call still an error

The last row is the point. A matcher reaching a real call raises MatcherLeaked at runtime, and a plugin that hid it would be worse than no plugin at all.

What else it reports

Reported as For
UnderstudyMisuse A matcher whose kind the parameter can never accept. A closure that specifies nothing, or two calls, or calls a static method. Cardinality no run can satisfy. verify() arguments that contradict each other.
Psalm's own InvalidArgument returns() and answers() against the method being specified. The plugin does not check these — it fills in the builder's template parameter, and WhenBuilder<TReturn> already declares returns(TReturn ...). Psalm does the rest.
Psalm's own array and method diagnostics wire(), whose shape is read from the named class's constructor: an unknown key is an error, and each double is typed as its contract. A dynamic class-string is left alone.

Everything the plugin is not sure about stays silent. A false accusation costs more than a missed one here, because the engine still catches what static analysis misses.

API

Type Purpose
UnderstudyMisuse The issue every diagnostic of this plugin is reported as. One type rather than one per rule: you either have understudy analysing your specifications or you do not, and needing to silence each rule separately would be a worse contract than needing to silence none.
Plugin The entry point Psalm loads, registered through extra.psalm.pluginClass. Nothing else in this package is public — the handlers it registers are @internal, and what they decide is the plugin's behaviour, not its API.

The understudy family

Package What it is
rasuvaeff/understudy The engine: doubles, matchers, expectations, verification.
rasuvaeff/understudy-testo Testo adapter — verification and reset around every test.
rasuvaeff/understudy-phpunit PHPUnit and Pest adapter — the same, through a trait.
rasuvaeff/understudy-psalm (this package) Psalm plugin — matcher-aware specifications and misuse diagnostics.
rasuvaeff/understudy-phpstan PHPStan extension — the same for PHPStan, plus its own rules.

Development

make build          # validate + cs + psalm + unit + integration
make test-integration

The integration suite runs real Psalm processes over a fixture project, twice: once with the plugin and once without. The run without it is what proves the run with it means anything — a plugin that loads and does nothing passes a positive fixture exactly as well as one that works.

License

BSD-3-Clause.