Skip to content
This repository was archived by the owner on Aug 23, 2026. It is now read-only.

refactor(quality): Rector wave 8 — route/dispatch/compact helpers; ThrowWithPrevious proven harmful (AID-543) - #195

Merged
abkrim merged 2 commits into
mainfrom
abdelkarim/aid-543-rector-helpers-exceptions
Jul 18, 2026
Merged

abkrim merged 2 commits into
mainfrom
abdelkarim/aid-543-rector-helpers-exceptions

Conversation

@abkrim

@abkrim abkrim commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Eighth and final wave of the split set. 13 app/ files (9 to_route, 11 dispatch(new …), 6 event(new …), 1 compact). Resized with --clear-cache: 4 + 8 + 1, overlap zero.

Equivalence is literal in vendor — verified per transform

Transform Proof
redirect()->route() → to_route() to_route() is redirect()->route() (alias in Foundation/helpers.php); ->with() chains on the same RedirectResponse
Event::dispatch() → event(new …) Events\Dispatchable::dispatch() is event(new static(...))
Job::dispatch() → dispatch(new …) Bus\Dispatchable::dispatch() → new PendingDispatch($job), identical to the helper; no converted site chains ->onQueue()/->onConnection()
compact('report') → ['report' => $report] variable provably in scope

ThrowWithPreviousExceptionRector: permanently skipped, proven harmful

All 9 domain throws already pass previous: — the rule's nominal purpose is satisfied. What it actually adds here is $e->getCode() as the second positional argument, assuming the stock \Exception(message, code, previous) signature. The domain exceptions have custom constructors: FirewallException's 2nd parameter is ?string $hostName, CsfServiceException's is ?string $message.

Under app/'s strict_types (wave 1) that is a runtime TypeError on the firewall error paths — reproduced in an isolated strict-mode script: Argument #2 ($hostName) must be of type ?string, int given. Without strict_types it would be silent data corruption (hostName='0'). The skip lives in config with the reason; a counter-probe confirms the applied wave touches zero throw sites and adds zero getCode() calls.

First commit: latent type bug surfaced and fixed at source

Moving the event constructor's call site from vendor (coercive) into app/ (strict) exposed RateLimiter::attempts() returning a string into an int parameter — it had only ever worked because Dispatchable::dispatch() built the event inside vendor, where PHP coerced '3' → 3 silently. The suite caught it (5 failures in ThrottleSimpleUnblockTest): exactly the strict_types safety net working. Fixed by casting at the data source ((int) RateLimiter::attempts() + the one uncast config() read in the same method — checkRateLimit() already casts its own).

Gates

  • Probe file with all three old forms → rc=2 naming each rule ✓
  • Counter-probe of the skip: 0 throws touched, 0 getCode() added ✓
  • Idempotent · refactor:dry (cleared cache) exit 0 ✓
  • composer check-full → exit 0, 1442 passed / 16 skipped, coverage 89.9% ✓

abkrim and others added 2 commits July 18, 2026 08:09
SimpleUnblockRateLimitExceeded declares int for $attempts and
$maxAttempts, but rateLimitResponse() fed it RateLimiter::attempts()
raw — whatever the cache store holds, a string under the database
driver — and an uncast config() read that returns the raw env string
whenever the variable is set. It only ever worked by accident: the
event was built through Dispatchable::dispatch(), whose call site
lives in vendor without strict_types, so PHP coerced '3' to 3
silently. checkRateLimit() in the same middleware already casts its
config read; this path was the exception.

Surfaced by the wave in the next commit, which moves the constructor
call site into this file — where declare(strict_types=1) (AID-529)
turns the hidden coercion into a TypeError, caught by the suite
(5 failures in ThrottleSimpleUnblockTest). The bug predates the wave;
the cast belongs at the data source either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rowWithPrevious proven harmful (AID-543)

Enable RedirectRouteToToRouteHelperRector, DispatchToHelperFunctionsRector
and CompactToVariablesRector; apply with cleared cache: 13 app/ files
(9 to_route, 11 dispatch(new ...), 6 event(new ...), 1 compact).

Equivalence is literal in vendor, verified per transform as the ticket
required: to_route() IS redirect()->route() (alias in
Foundation/helpers.php; the ->with() chaining lands on the same
RedirectResponse); event Dispatchable::dispatch() IS
event(new static(...)); Bus Dispatchable::dispatch() returns
newPendingDispatch(new static(...)) = new PendingDispatch($job),
identical to the dispatch() helper — and no converted site chains
->onQueue()/->onConnection(). compact('report') -> ['report' => $report]
with the variable provably in scope.

ThrowWithPreviousExceptionRector is PERMANENTLY skipped, proven harmful
rather than assumed safe: all 9 domain throws already pass previous:,
so what the rule actually adds here is $e->getCode() as the SECOND
POSITIONAL argument, assuming the stock \Exception(message, code,
previous) signature — but the domain exceptions have custom
constructors (FirewallException's 2nd parameter is ?string $hostName,
CsfServiceException's is ?string $message). Under app/'s strict_types
that is a runtime TypeError on the firewall error paths, reproduced in
an isolated strict-mode script: "Argument #2 ($hostName) must be of
type ?string, int given". Without strict_types it would be silent data
corruption (hostName='0'). The skip is config, not prose; a
counter-probe confirms the applied wave touches zero throw sites and
adds zero getCode() calls.

The dispatch conversion surfaced a latent type bug fixed in the
previous commit: moving the event constructor's call site from vendor
(coercive) into app/ (strict) exposed RateLimiter::attempts()
returning a string into an int parameter — the suite caught it
(5 failures), exactly the strict_types safety net working as intended.

Wave checklist:
- resized with --clear-cache: 4 + 8 + 1 files, overlap zero, 13 total
- diff shape-verified: every changed line classified, none unexplained
- rules bite, attributably: a probe file with all three old forms ->
  exit 2 naming each rule
- idempotence: second rector+pint pass is a byte-level no-op
- gates: check-full green, 1442 passed / 16 skipped, coverage 89.9%,
  refactor:dry (cleared cache) exit 0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@abkrim
abkrim merged commit 3529e15 into main Jul 18, 2026
2 checks passed
@abkrim
abkrim deleted the abdelkarim/aid-543-rector-helpers-exceptions branch July 18, 2026 06:21
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant