Skip to content

Refuse what used to be a fatal, and stop printing what must not be printed - #112

Merged
rasuvaeff merged 1 commit into
masterfrom
fix/wave-1
Sep 5, 2026
Merged

Refuse what used to be a fatal, and stop printing what must not be printed#112
rasuvaeff merged 1 commit into
masterfrom
fix/wave-1

Conversation

@rasuvaeff

Copy link
Copy Markdown
Owner

Fixes #102. Fixes #103. Fixes #104. Fixes #105. Fixes #106. Fixes #107. Fixes #108. Fixes #109. Fixes #110. Fixes #111.

Batched into one branch: the findings are independent but the mutation gate is one run, and every one of them is a src/ or docs change in the same package.

Uncatchable fatals (#102, #103)

Understudy::for(Throwable::class) — and UnitEnum, BackedEnum, DateTimeInterface, Traversable — walked past every refusal in DoubleFactory and was answered by the compiler instead: Fatal error: … cannot implement interface Throwable out of eval(). Not catchable by try, not by an adapter, and fatal to the whole suite run rather than to one test. A clock and an exception are among the first contracts anyone tries to double.

Each is now an UnsupportedTarget naming the way through. Iterator, IteratorAggregate, Stringable and Countable still double — being built in was never the reason, and a test pins that.

Understudy::for(A::class, A::class) produced the same fatal (implements A, A does not compile); the list is deduplicated before generation.

Secrets in the log (#108)

A parameter the contract marks #[\SensitiveParameter] is rendered as its type — login('user', string SensitiveParameter) — in failure messages and in transcript(). The value used to go into both verbatim. The positions are resolved once, at generation time, with the rest of the reflection, and carried on the Invocation.

The expectation half of the message is not redacted, and both Security sections now say so: those are the literals the test itself wrote, and they are in the test file already.

The exception contract (#107, #106)

UnderstudyError says it is implemented by every exception this library throws, and llms.txt tells users to catch it. Three paths threw a bare \InvalidArgumentException: Cardinality's two bounds checks and returns() with no values. InvalidSpecificationArgument extends \InvalidArgumentException and implements UnderstudyError, so a catch by the SPL type written yesterday keeps working.

Arg::instanceOf('Nope\Missing') throws it too. It used to match nothing, forever, and say so nowhere — the reader saw "expected … but it was never called" and looked for the cause in the subject under test, while Understudy::for() refuses the same input in the same breath.

The formatter (#104, #105)

(string) NAN warns on PHP 8.5 — from inside the library, while it renders a message about a failure, which under failOnWarning turns the report into a different failure. And control bytes travelled into the message and the transcript raw: a NUL, or half a broken UTF-8 sequence, breaking the single line the escaping exists to keep. Valid multibyte text is untouched, and the cut marker is never escaped as payload.

Documentation (#109, #110, #111)

checkpoint() clears only the settled calls — the code was right and the text was not, in README.md, README.ru.md and llms.txt alike. A declared property default survives while a promoted one does not (and $double->promoted therefore differs from the real object). A built-in interface as a return type does not become a nested double. Arg::string() carries PCRE's own meaning of $. CannotWire and InvalidDefaultValue say "has type array" rather than "is a array". The broken cookbook link in examples/README.md and the dead _plans/ reference in docs.yml are gone.

The committed docs/src/api/** and the snapshot are regenerated from the current src/; they were built from a v0.5.0 snapshot and documented an Invocation constructor that no longer exists. COMPLETENESS_BUDGET drops in two categories, as the checker asked.

Verification

  • composer build green.
  • make mutation green: Covered Code MSI 94% against the minMsi = 92 gate, 182 escaped of 3067 — the level the package was at before this branch (the five refusal messages are asserted whole rather than by fragment, which is what pays for the new branches).
  • make docs-build green, including the integrity check and the anchor check.

…inted

Understudy::for() on Throwable, UnitEnum, BackedEnum, DateTimeInterface or
Traversable walked past every refusal in the factory and was answered by the
compiler: a fatal out of eval(), uncatchable by try or by an adapter, and fatal
to the whole suite run rather than to one test. A clock and an exception are
the first two contracts anybody reaches for. Each is now an UnsupportedTarget
naming the way through; Iterator, IteratorAggregate, Stringable and Countable
keep doubling, because being built in was never the reason. A contract named
twice in one list produced the same fatal and is now deduplicated.

A #[\SensitiveParameter] is rendered as its type, the way PHP redacts one in
its own traces. Its value used to go verbatim into the failure message and into
transcript(), which is to say into a CI log.

Three public paths threw a bare \InvalidArgumentException while UnderstudyError
declares itself implemented by every exception this library throws -- so the
catch llms.txt recommends walked past them. InvalidSpecificationArgument
extends \InvalidArgumentException, so a catch by the SPL type keeps working, and
it is what Arg::instanceOf() now throws for a class that is not loadable
instead of matching nothing forever and saying so nowhere.

The formatter no longer raises a PHP warning while rendering a failure --
(string) NAN warns on 8.5 -- and escapes control bytes and binary strings, which
used to break the single line the escaping exists to keep.

Documentation follows the code where the code was right: checkpoint() clears the
settled calls, not the log; a declared property default survives and a promoted
one does not; a built-in interface as a return type does not become a nested
double; Arg::string() carries PCRE's own meaning of $. Both Security sections
now say that arguments are printed verbatim except sensitive ones. The committed
API pages are regenerated from the current src/ rather than a v0.5.0 snapshot.

Fixes #102
Fixes #103
Fixes #104
Fixes #105
Fixes #106
Fixes #107
Fixes #108
Fixes #109
Fixes #110
Fixes #111
@coderabbitai

coderabbitai Bot commented Sep 5, 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: Team

Run ID: dd8efc38-f8c2-491c-b2ca-edb43f1efddf

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


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 e19702e into master Sep 5, 2026
22 checks passed
@rasuvaeff
rasuvaeff deleted the fix/wave-1 branch September 5, 2026 10:39
@rasuvaeff rasuvaeff mentioned this pull request Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment