Add PHPStan + larastan static analysis #55
Labels
No labels
bug
duplicate
enhancement
good first issue
help wanted
question
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lvl0/incr#55
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
incr has no static analysis at all — no
phpstan.neon, and neitherphpstan/phpstannorlarastan/larastanincomposer.json. ffr runs level 7 in CI.Why this matters now
Every other v0.4.0 ticket lists "PHPStan clean" in its acceptance criteria, and
container_analyseis a standard gate in the global workflow config. That gate does not currently exist in this project, so those checkboxes are unsatisfiable until this lands.Scope
Mirror ffr's setup:
composer require --dev larastan/larastan phpstan/phpstan— match ffr's constraints (larastan ^3.5,phpstan ^2.1.32 <2.2). Skipphpstan-mockeryunless mocks appear in the tests from #54.phpstan.neonat level 7,paths: [app/, tests/], excludingbootstrap/*.phpandstorage/*phpstan-baseline.neonand include it.forgejo/workflows/ci.ymlSequencing
Land this after the deletion tickets (#47–#53), not before. A baseline generated now would be mostly noise about code that is about to be removed — and the whole point of a baseline is that it stays small and shrinks.
Practical consequence: earlier tickets' "PHPStan clean" criteria are aspirational until this lands. Treat them as "lint + tests clean" until then, and let this ticket be the one that makes the gate real.
Acceptance criteria
vendor/bin/phpstan analyseruns clean at level 7container_analyseMCP tool works against this projectDone —
d2dfe44Static analysis exists now. Level 7, zero errors, no baseline file.
larastan/larastan ^3.5(→ 3.9.6) andphpstan/phpstan ^2.1.32 <2.2(→ 2.1.56), matching ffrphpstan.neon— level 7, pathsapp/,database/,tests/--error-format=githubfor inline annotationsphpstan-mockery— the suite uses factories and real HTTP calls, no mocksDeviation from the ticket:
database/is included in the analysed paths, which ffr does not do. The migrations now carry real logic (theSUM(quantity)backfill loop) and the factories back every test. All 12 migrations are clean.The 17 errors it found
Real typing improvements, not suppressions:
@return BelongsTo<User, $this>onTracker::user(),@return HasOne<Tracker, $this>onUser::tracker()CounterController::increment()—$tracker->refresh()->counttripped "undefined propertyModel::$count" becauserefresh()erases toModel. Split into$tracker->refresh();then$tracker->count. Behaviour identical.One genuine Laravel quirk, ignored deliberately: migration files return an anonymous class defining
up()/down(), but the abstractMigrationbase declares neither, so the call cannot be typed. Narrowly-scopedignoreErrorsmatched totests/Feature/*MigrationTest.phpand only that message.Why not an interface — tested, not assumed
Review pushed back on the ignore rule and proposed a marker interface (
extends Migration implements ReversibleMigration), asking why I had dismissed it.I built it, and the tests passed — which is precisely the danger.
Tests\is underautoload-dev, anddocker/production/Dockerfile:58runscomposer install --no-dev. Every migration would fatal with "Interface not found" on deploy, and the test suite could never catch it because tests always have dev autoload.That reasoning is now a comment in
phpstan.neonso the next person does not re-attempt it.Gate verified against injected errors
$tracker->nonexistentPropertyin app code → caught asApp\Models\Tracker::$nonexistentProperty, not the vagueModel::seen before the generics were added, confirming inference genuinely improvedbogusMethod()inside the file the ignore rule covers → still caught, confirming the rule suppresses onlyup()/down()Level 8
Tested rather than guessed: 8 errors across 4 files. Two of those files are deleted by #53 and #52, one of the errors being a real null-safety bug (
$this->user()->idin an app with no auth). After those tickets, level 8 costs about fiveassertNotNull()calls in tests. Noted on #53 and #52.Gates
PHPStan 0 errors (level 7) · Pint PASS 48 files · PHPUnit OK 31 tests, 81 assertions ·
container_analyseworks against this project