Skip to content

chore: raise PHPStan to level 10 - #135

Merged
dakorpar merged 1 commit into
masterfrom
chore/phpstan-level-10
Aug 14, 2026
Merged

chore: raise PHPStan to level 10#135
dakorpar merged 1 commit into
masterfrom
chore/phpstan-level-10

Conversation

@dakorpar

Copy link
Copy Markdown
Member

Follow-up to #134, and the last step — 10 is PHPStan's maximum level. It treats implicit mixed the same way level 9 treats explicit mixed.

All 17 errors had the same origin: reading back through Nette's magic accessors, which are mixed by nature.

The class[] idiom, in one place

Nine of the errors were $el->class[] = '...' — spread across seven files, each preceded (or, in two cases, not preceded) by its own BootstrapUtils::standardizeClass() call. It worked because Nette's Html::__get() returns by reference, so the append auto-vivified the attribute.

BootstrapUtils now carries that knowledge instead:

BootstrapUtils::fetchClasses($el);       // classes as a list, however the attribute was set
BootstrapUtils::addClass($el, 'form-control');
BootstrapUtils::removeClass($el, 'ajax');
BootstrapUtils::hasClass($el, 'ajax');

Call sites now read as what they do. BootstrapRenderer::fetchClasses(), which #134 added, was this same logic a second time and now delegates to the util. standardizeClass() is kept — it is public API — though nothing in src/ needs it any more.

The rest

  • BootstrapForm::setAjax() did the class juggling by hand (standardizeClass → read → in_arrayarray_diff → write back). It goes through the util now and keeps its "only add if absent" behaviour, which a new test pins down.
  • The onError handler took an untyped $form with @param BootstrapForm $form in a docblock positioned where PHPStan could not attach it. It is a real parameter type now.
  • renderBegin() read $el->action three times, casting mixed to string and carrying two @noinspection comments. It reads the attribute once into a local and normalizes it there — which also handles an action set as a Nette\Application\UI\Link rather than a plain string.
  • addInputError() called addError() on whatever getComponent() returned. It now checks for a BaseControl and throws InvalidArgumentException naming the component when given something that cannot hold an error (a container, say), instead of dying on an undefined method.
  • UploadInput loses trim($control->class .= ...), which relied on the class attribute being a string and on .= evaluated inside a function call.

Verification

  • make phpstan (now -l 10, the maximum) — no errors
  • make cs — clean
  • make tests — 181 tests, 277 assertions, all passing

8 new tests: six cover the new BootstrapUtils methods (both attribute shapes, absent attribute, removing a class that is not there), one that setAjax(true) twice does not duplicate the class, one for the addInputError guard.

Rendered HTML is unchanged — no fixture updates. The snapshot suite is the safety net for the class[] refactor: every one of those seven files feeds into fixtures that still match byte for byte.

🤖 Generated with Claude Code

Level 10 is the last one — it treats implicit `mixed` the same way level 9
treats explicit `mixed`. All 17 errors were the same shape: reading back
through Nette's magic accessors, which are typed `mixed` by nature.

Nine of them were the `$el->class[] = '...'` idiom, spread across seven
files, each preceded (or not) by its own `standardizeClass()` call. That is
now one place: `BootstrapUtils` grew `fetchClasses()`, `addClass()`,
`removeClass()` and `hasClass()`, and the call sites say what they mean.
`BootstrapRenderer::fetchClasses()`, added at level 9, was the same logic
again and now delegates to the util.

The rest:

- `BootstrapForm::setAjax()` used the same class juggling by hand; it now
  goes through the util and keeps its "only add if absent" behaviour.
- The `onError` handler took an untyped `$form` with the type in a docblock
  that PHPStan could not attach. It is a real parameter type now.
- `renderBegin()` read `$el->action` three times, casting `mixed` to string.
  It reads the attribute once into a local and normalizes it there, which
  also covers an action set as a `Nette\Application\UI\Link`.
- `addInputError()` called `addError()` on whatever `getComponent()`
  returned. It now says what it needs, and explains itself when given a
  component that cannot hold an error (a container, say) instead of dying
  on an undefined method.

`UploadInput` also loses a `trim($control->class .= ...)`, which relied on
the class attribute being a string and on `.=` inside a function call.

Rendered HTML is unchanged — no fixture updates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.54%. Comparing base (faea76b) to head (4790aa6).

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #135      +/-   ##
============================================
+ Coverage     97.51%   97.54%   +0.02%     
- Complexity      333      340       +7     
============================================
  Files            25       25              
  Lines          1007     1019      +12     
============================================
+ Hits            982      994      +12     
  Misses           25       25              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dakorpar
dakorpar merged commit f6650b4 into master Aug 14, 2026
11 checks passed
@dakorpar
dakorpar deleted the chore/phpstan-level-10 branch August 14, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant