Skip to content

Psalm plugin

rasuvaeff/understudy-psalm teaches Psalm the one thing about understudy that looks wrong and is not.

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

This page covers the Psalm side. The shared reasoning — why a matcher is declared mixed, and why the leak has to stay reported — is on Static analysis.

The report it drops, and where

Arg::int() is declared mixed, because a matcher has to be passable wherever the contract declares anything at all. The plugin drops Psalm's argument-type report inside a specification closure, and only there:

WherePsalm
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 callstill an error

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

The two 0.4 idioms

  • Arg::rest() legitimately passes fewer arguments than the contract declares, so TooFewArguments goes quiet on that call — but only when the call sits inside a specification and its last written argument is Arg::rest(). A real call ending in Arg::rest() is under-arity for real (the engine answers it with ArgumentCountError) and keeps both reports.
  • Arg::captor()'s $captor->capture() is a matcher in method-call clothes: its argument-type report goes quiet inside a specification, it does not count against "exactly one call per closure", and a capture leaked into a real call keeps its report.

What else it reports

Reported asFor
UnderstudyMisusea 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 InvalidArgumentreturns() 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 diagnosticswire(), 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

Silent when unsure

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

API

MemberPurpose
Pluginthe entry point Psalm loads
UnderstudyMisusethe issue type every finding of the plugin's own rules is reported under

Everything else in the package is internal to the plugin.