Skip to content

Fix the 2026-09-18 review findings: fatal-free refusals, nested calls, returns() checks - #139

Merged
rasuvaeff merged 3 commits into
masterfrom
fix/review-2026-09-18
Sep 17, 2026
Merged

rasuvaeff merged 3 commits into
masterfrom
fix/review-2026-09-18

Conversation

@rasuvaeff

Copy link
Copy Markdown
Owner

One PR for the whole wave of the 2026-09-18 family review: the two class A defects, the class B ones the engine could answer, and the divergences that are written down rather than changed.

Change Issue
An interface extending Throwable, DateTimeInterface, UnitEnum/BackedEnum or Traversable (without Iterator/IteratorAggregate), and an interface declaring __construct, are refused with UnsupportedTarget before eval(). The guard compared the contract's own name, so Psr\Http\Client\ClientExceptionInterface walked past it and died in the compiler as a fatal no test, adapter or try can catch. Iterator/IteratorAggregate keep doubling; the Throwable advice no longer points at doubling an exception class (refused too). Fixes #133
when(fn () => $r->find($r->count())) is refused naming both calls. Recording ends on the first dispatch — the innermost call — so count() was stubbed silently with find()'s returns(). The closure is re-run once in a probe mode that answers every call with the mode's default out of a throwaway context; any throwable ends the probe as inconclusive (a : never method, a return type without a default, code after the call that does not survive a default) and the specification stands as before. Reference-returning methods get a throwaway slot in probe mode so the per-double slot is untouched. Fixes #134
returns() is checked against the declared return type where it is written: a value on : void (returns(null) stays allowed — it is the idiom for "answer nothing" and the package's own tests use it), anything on : never, and a value the type cannot hold (null on a non-nullable type, array/object on a scalar, wrong class) raise InvalidCallSpecification/InvalidSpecificationArgument naming the double. No stricter than the engine: generated methods are not under strict_types, so returns('5') on : int still answers 5. answers() is judged only at the call. WhenBuilder::__construct() gains an optional trailing parameter. Fixes #135
The second and later unlabelled doubles of one contract in a context are numbered — Repo, Repo#2; the first keeps the bare name, label() still wins. Fixes #137
ForgottenDouble after a scope() closed names the scope instead of a reset() the test never wrote. Fixes #138
Documented, not changed (README EN/RU, llms.txt): a catch-all stub after a specific one is not diagnosed — the engine's own property model treats "later wins" as law, and refusing it broke three property tests, so #136 is closed as documented; the depth-1 default double is fresh per call; a class double is never a partial double of an abstract class; readonly promoted properties stay uninitialized; MatcherLeaked is raised only by a double; Arg::rest() is the way to stop spelling a wide signature. #136

Checks: composer build (1107 tests), composer rector, git diff --check; make mutation — 3415 mutants (was 3244; ReturnContract and the builders joined the #[Covers] map), Covered Code MSI 93 % at gate 92, seven new escapes classified as equivalent in AGENTS.md.

…, returns() checks

- Refuse an interface extending Throwable, DateTimeInterface, UnitEnum
  or Traversable (without Iterator/IteratorAggregate), and an interface
  declaring __construct, with UnsupportedTarget before eval(); the guard
  compared the contract's own name and let ClientExceptionInterface die
  in the compiler as an uncatchable fatal. Fixes #133
- Refuse a specification closure that calls one double method while
  evaluating the arguments of another, naming both: the recording ended
  on the innermost call and stubbed it silently with the outer call's
  returns(). The closure is re-run once in a probe that answers calls
  with defaults out of a throwaway context; where the probe cannot
  finish, the specification stands as before. Fixes #134
- Check returns() against the declared return type at registration —
  a value on void (null stays allowed), anything on never, a value the
  type cannot hold — naming the double instead of a TypeError naming
  the generated class from inside the code under test. No stricter than
  the engine: generated methods are not under strict_types. Fixes #135
- Number the second and later unlabelled doubles of one contract
  (Repo, Repo#2) so a report says which one. Fixes #137
- Say "scope() closed" rather than "reset()" in ForgottenDouble after a
  scope dropped the double. Fixes #138
- Document rather than change: a later catch-all stub is not diagnosed
  (#136), the depth-1 default double is fresh per call, no partial
  double of an abstract class, readonly promoted properties, matcher
  leaks into real objects, Arg::rest() for wide signatures.
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 679485bb-1f5b-4614-ab66-f1e7f370a786


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rasuvaeff
rasuvaeff merged commit 7775f91 into master Sep 17, 2026
20 checks passed
@rasuvaeff
rasuvaeff deleted the fix/review-2026-09-18 branch September 17, 2026 23:54
@rasuvaeff rasuvaeff mentioned this pull request Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment