Download the PHP package rasuvaeff/understudy-phpstan without Composer
On this page you can find all versions of the php package rasuvaeff/understudy-phpstan. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download rasuvaeff/understudy-phpstan
More information about rasuvaeff/understudy-phpstan
Files in rasuvaeff/understudy-phpstan
Package understudy-phpstan
Short Description PHPStan extension for the understudy test double library: matcher-aware specification closures and misuse rules
License BSD-3-Clause
Homepage https://github.com/rasuvaeff/understudy-phpstan
Informations about the package understudy-phpstan
rasuvaeff/understudy-phpstan
PHPStan extension for understudy.
Using an AI coding assistant? Point it at llms.txt.
Requirements
- PHP 8.3 - 8.5
phpstan/phpstan^2.2.2nikic/php-parser^5.0rasuvaeff/understudy^0.9 || ^0.10
Installation
With phpstan/extension-installer that is all. Without it, include the extension yourself:
Usage
understudy specifies a call by making it inside a closure:
Four things about that line are invisible to PHPStan on its own, and this extension is those four things.
1. A matcher fits whatever the contract declares
Arg::int() is declared mixed, because a matcher has to be passable
wherever a contract declares anything at all. At level 9 and above PHPStan
reports it as Parameter #1 $id … expects int, mixed given — correct about
the type, wrong about the code.
The extension types every matcher as never, the bottom type, which every
parameter accepts. Nothing is suppressed. A wrong argument beside a
matcher, a method the double does not have, the statements around the
closure — all keep their reports:
Below level 9 there is nothing to fix here — PHPStan does not check mixed
against a declared parameter — and the rest of the extension works at every
level.
A specification is any of understudy's call-closure verbs, in either
spelling: the free functions when(), expect(), expectSequence() and
verify(), the same names on Understudy::, and the readers that exist only
there — calls(), lastCall() and verifySequence(). Every closure of a
protocol counts, so a matcher in the third step of an expectSequence() is
read exactly like one in a when().
Two more idioms are covered the same way:
Arg::rest()legitimately passes fewer arguments than the contract declares —when(fn () => $storage->recordOutcome('svc', Arg::rest()))— so thearguments.countreport is ignored wherever a call's last written argument isArg::rest(). In a real call that is still a mistake, and it is reported as the mistake it is:understudy.matcherLeaknames the leaked matcher (the engine answers the call itself withArgumentCountError).Arg::captor()'s$captor->capture()is a matcher in method-call clothes: typedneverlike theArg::factories (the receiver's type is what decides — a foreigncapture()is left alone), not counted against "exactly one call per closure", and reported byunderstudy.matcherLeakwhen it reaches a real call.Arg::captor()itself is a factory, not a matcher, and legitimately lives outside the closure.
2. returns() is checked against the method being specified
The core declares when(): WhenBuilder<mixed>, and it has no choice: which
method is being specified is known only from the closure. The extension fills
the template parameter in, and PHPStan does the rest:
3. wire() has the shape of the class it wired
The shape says what the core builds, and only that. A parameter typed
BookRepository&Auditor is ONE double standing for both contracts, so the key
keeps the intersection and both halves stay callable. A parameter typed
BookRepository|Clock makes the core refuse the class outright — CannotWire,
because picking one of the two would be a guess — so no shape describes the
call at all and the core's own array{sut: object, doubles: array<string, object>} stands. A dynamic class-string is left alone for the same reason.
4. Specifications that cannot work are reported
Each of these has a runtime counterpart — the engine throws, or the expectation never matches. Reporting them statically buys the one thing runtime cannot: a specification that can never match is exactly the mistake a green suite hides.
| Identifier | Reported when |
|---|---|
understudy.closure |
The closure specifies nothing, makes more than one call, or calls a static method a double cannot intercept |
understudy.cardinality |
times(5, 2), a negative bound, verify(…, never: true, times: 3), times beside a minimum |
understudy.matcher |
A matcher whose kind the parameter can never accept: Arg::int() where a string is declared |
understudy.returns |
returns() on a method declared void, where no value is ever observed |
understudy.matcherLeak |
A matcher written outside a specification and outside a closure, where it reaches the code as a value. A matcher hoisted into a variable, stored on a property or written in a closure handed over later is not one: the closure has not run, and the specification that runs it is elsewhere |
The rules are silent whenever they are not sure. A refined parameter type —
non-empty-string, an int range — answers "maybe" to its plain kind, and a
matcher can produce a value that fits it, so nothing is reported. A false
accusation costs more than a missed one here, because the engine still
catches at runtime what the extension misses.
To silence one of them, use its identifier:
Those identifiers are stable. They are what a consumer writes into their own
phpstan.neon, and renaming one would silently stop an ignoreErrors entry
somebody relies on; a new rule gets a new identifier rather than reusing an
existing one. The wording of a message is not stable and a patch release may
reword one — match on the identifier, never on the sentence.
Why understudy.matcherLeak exists
Typing every matcher as never is what lets one stand in for a typed
parameter, and it does so everywhere — including in a real call:
Without a rule for it the extension would be weaker than no extension for that mistake, because PHPStan would otherwise have reported the argument itself. Saying it directly is also better than the type error it replaces: at runtime the matcher reaches the code as a sentinel object, and the failure it eventually causes names neither the matcher nor the line.
Security
The extension runs inside PHPStan, reads source and reflection, and reports. It executes no code from the project under analysis and writes nothing.
Examples
See examples/README.md. The executable demonstration is
the set of fixture projects under tests/Integration/Fixtures, each analysed
by a real PHPStan process as part of composer build — including a control
run with the extension switched off, which is what tells a working extension
apart from one that loads and does nothing.
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 | Psalm plugin — matcher-aware specifications and misuse diagnostics. |
| rasuvaeff/understudy-phpstan (this package) | PHPStan extension — the same for PHPStan, plus its own rules. |
Development
License
BSD-3-Clause. See LICENSE.md.
All versions of understudy-phpstan with dependencies
nikic/php-parser Version ^5.0
phpstan/phpstan Version ^2.2.2
rasuvaeff/understudy Version ^0.8 || ^0.9