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

refactor(quality): Rector wave 1 — declare(strict_types=1) in app/ (AID-529) - #189

Merged
abkrim merged 1 commit into
mainfrom
abdelkarim/aid-529-rector-wave-strict-types
Jul 17, 2026
Merged

abkrim merged 1 commit into
mainfrom
abdelkarim/aid-529-rector-wave-strict-types

Conversation

@abkrim

@abkrim abkrim commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

The only semantic wave: strict_types changes runtime type coercion, so a file that today accepts "5" where it declares int starts throwing TypeError. Every other wave only changed shape.

73 files in app/ gain the declare → app/ is now 182/182, matching openmizan's convention.

Scope decision: Pint stays out

The ticket floated adding declare_strict_types to pint.json to own the convention repo-wide, on the premise that Rector alone would leave new files uncovered.

That premise was wrong. With the rule in the gate, a new app/ file without the declare fails refactor:dry — proven by the violation probe below. The gate already owns app/.

Pint would only extend reach outside app/. Measured (the ticket's "175 files in tests/" was also wrong — that counted files that exist, not files that would change):

Path short of the convention
tests/ 44 (not 175 — it is already at 131/175)
routes/ 2
bootstrap/ 6

That is a different mechanism with a different risk profile. It belongs to its own wave, not smuggled into a semantic PR.

Diff is shape-verified pure

added lines : 146 = 73 declares + 73 blank lines, zero others
deletions   :   0   (insert-only)

Semantic risk reviewed by hand

A green suite does not prove coercion is safe on uncovered paths, so every raw-input site among the 73 was traced to its destination.

Breakdown: 32 Filament · 10 artisan commands · 8 models · 7 exceptions · 3 services · 2 each of controllers/middleware/Livewire/traits.

Artisan commands are the classic hazard — argument() and option() always return strings:

Site Destination Verdict
AddHostKeyCommand:35 Host::where('id', $hostId) Eloquent accepts string ✓
UnblockIpCommand:47 $action->handle($ip, (int) $hostId, ...) already casts ✓
TestHostConnectionCommand:40 Host::find($hostId) Eloquent accepts string ✓
TestSpatieMultiplexerCommand:29 Host::find($hostId) Eloquent accepts string ✓

Remaining option() uses are boolean flags; request()->ip() returns string. No string reaches a typed int/float parameter uncast.

Wave checklist

  • ✅ Clean tree at start; applied via composer refactor
  • ✅ Rule bites, attributably: new file without declare → rc=2 naming app/ProbeStrict.php:1
  • ✅ Idempotent: second pass is a byte-level no-op (ordered checksum identical, rector reports 0 changes)
  • ✅ composer check-full → exit 0, 1442 passed / 16 skipped, coverage 89.8%, PHPStan clean, refactor:dry exit 0

…ID-529)

Enable DeclareStrictTypesRector and apply it via composer refactor: 73
files in app/ gain declare(strict_types=1), bringing app/ to 182/182.

This is the only semantic wave: strict_types changes runtime type
coercion, so a file that today accepts "5" where it declares int starts
throwing TypeError. Every other wave only changed shape.

Scope decision — Pint stays out of it. The ticket floated adding
declare_strict_types to pint.json to own the convention repo-wide, on
the premise that Rector alone would leave new files uncovered. That
premise was wrong: with the rule in the gate, a new app/ file without
the declare fails refactor:dry (proven by violation probe below), so the
gate already owns app/. Pint would only extend reach OUTSIDE app/ —
measured at 44 files in tests/, 2 in routes/, 6 in bootstrap/ — which is
a different mechanism with a different risk profile and belongs to its
own wave, not smuggled into a semantic PR.

Wave checklist (per rector.php header):
- clean tree at start; applied via composer refactor (rector + pint)
- diff shape-verified pure: 146 added lines = 73 declares + 73 blank
  lines, zero others, zero deletions (insert-only)
- rule proven to bite, attributably: a new file without the declare ->
  exit 2 naming app/ProbeStrict.php:1
- idempotence proven: second composer refactor pass is a byte-level
  no-op (ordered checksum identical, rector reports zero changes)
- gates: check-full green, 1442 passed / 16 skipped, coverage 89.8%,
  PHPStan clean, refactor:dry exit 0

Semantic risk reviewed by hand, since a green suite does not prove
coercion is safe on uncovered paths. The 73 files break down as 32
Filament, 10 artisan commands, 8 models, 7 exceptions, 3 services, 2
each of controllers/middleware/Livewire/traits. Artisan commands are the
classic hazard — argument() and option() always return strings — so
every raw-input site was traced to its destination: Host::where('id',
$hostId) and Host::find($hostId) pass strings to Eloquent (accepted);
UnblockIpCommand already casts with (int) $hostId before the typed call;
the remaining option() uses are boolean flags and request()->ip() is
string. No string reaches a typed int/float parameter uncast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@abkrim
abkrim merged commit 07a8946 into main Jul 17, 2026
2 checks passed
@abkrim
abkrim deleted the abdelkarim/aid-529-rector-wave-strict-types branch July 17, 2026 15:03
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