chore(quality): migrate to PHPStan 2 — 35 findings to zero, plus a missing-manager bug - #697
Merged
Conversation
added 2 commits
August 22, 2026 02:09
…al bug
Bumps `phpstan/phpstan` to ^2.0 and `conduction/hydra-gates` to ^1.8.2,
and clears every finding the new major surfaces.
## A missing manager was never detected
ContactPersonHandler::setUserManager():
$user = $this->_userManager->get($username);
$manager = $this->_userManager->get($managerUsername);
if ($user === null || $manager === false) { ...warn and return... }
IUserManager::get() returns `?IUser` — it signals "no such user" with
NULL and never returns false. So the second half of that guard could
never fire: calling setUserManager() with a manager username that does
not exist skipped the warning entirely and carried on as if the manager
were real. Fixed to `=== null`.
PHPStan found it from the other end: inside that branch `$user !== null`
was reported as always FALSE, because the only reachable way in was the
first clause.
## Dead guards (28)
Mostly in the ArchiMate import/export pair, which are near-copies of each
other, so nearly every finding came in twos:
- `self::PERFORMANCE_OPTIMIZATIONS['memory_cleanup'] !== false` and
`['parallel_processing'] === true` — both are class constants set to
true, so neither was ever conditional.
- `$identifier !== false`, `$versionId !== false`, `$refCompId !== false`,
`$amefKey !== false` — all strings; none can be false.
- `is_array($sectionData) === false` — the parameter is declared `array`,
so PHP rejects anything else at the call boundary first.
- `isset($statistics[$sectionKey]) === false` — the branch above pins
$sectionKey to a key $statistics always has.
- `if ($section !== 'omschrijving')` with the comment "Skip summary
section itself" — `omschrijving` is assigned to $statistics on the line
AFTER the loop, so the loop can never see it.
- `method_exists($this->archiMateService, '...Optimized')` — the method
is declared on the class.
- Several `isset() && !== null` pairs and non-nullable-entity null tests.
## One scoped ignore
OrganizationSyncService's `if ($contactObject !== null)` is provably true
— the code a few lines above already dereferences $contactObject
unconditionally. It is left in place because the block it wraps is 243
lines: removing the `if` is a pure re-indentation of a quarter of the
method, a large review-hostile diff for zero behaviour change. The
comment says so and marks it for the next real edit to that method.
## Verification
phpstan 0, phpcs clean, phpmd clean.
PHPUnit is NOT part of this evidence: the bootstrap requires a booted
Nextcloud (`Class "OC_App" not found`) and cannot run standalone. Checked
that this is pre-existing by stashing every change in this commit and
re-running — byte-identical failure. CI runs the suite inside the
container.
CI's phpcs step runs `--warning-severity=0`, and it failed on every one
of the three PRat in this series for the same reason: comments I wrote.
Two sniffs:
- Squiz.Commenting.InlineComment.NotCapital — many of my new comments
open with a lowercase function name ("// find() throws rather than
..."). Rephrased so the first word is a real capitalised word.
- Generic.Commenting.DocComment.TagsNotGrouped — the `@param-out` tags I
added were interleaved between `@param` tags, splitting the group.
Moved below the last `@param`. One of those inserts had also orphaned a
continuation line off the `@param` above it; that is rejoined.
Where PHPStan genuinely needs a `/** @var */` inline doc-block (which
Squiz.Commenting.InlineComment.DocBlock rejects), the line now carries a
targeted `phpcs:ignore` naming that sniff and saying why, rather than
dropping the annotation and leaving the type wrong.
I should have caught this locally. I did run phpcs, but with
`--report=summary | tail -3`, which prints only the timing line — so I
read an empty tail as "clean" when the error count was two lines above
the cut. Re-verified here with CI's exact invocation.
Contributor
Quality Report — ConductionNL/stackiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue-demi | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 130/130 | |||
| npm | ✅ | ✅ 703/703 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 00:22 UTC
Download the full PDF report from the workflow artifacts.
Contributor
Quality Report — ConductionNL/stackiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue-demi | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 130/130 | |||
| npm | ✅ | ✅ 703/703 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-22 00:39 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps
phpstan/phpstanto^2.0andconduction/hydra-gatesto^1.8.2, and clears every finding the new major surfaces: 35 → 0.A missing manager was never detected
ContactPersonHandler::setUserManager():IUserManager::get()returns?IUser— it signals "no such user" with null, neverfalse. So the second half of that guard could never fire: callingsetUserManager()with a manager username that does not exist skipped the warning entirely and carried on as if the manager were real. Fixed to=== null.PHPStan found it from the other end: inside the branch,
$user !== nullwas reported as always false, because the only reachable way in was the first clause.Dead guards (28)
Mostly in the ArchiMate import/export pair, which are near-copies of each other — so nearly every finding came in twos:
PERFORMANCE_OPTIMIZATIONS['memory_cleanup'] !== false,['parallel_processing'] === truetrue$identifier !== false,$versionId !== false,$refCompId !== false,$amefKey !== falsefalseis_array($sectionData) === falsearray, so PHP rejects anything else at the call boundaryisset($statistics[$sectionKey]) === false$sectionKeyto a key$statisticsalways hasif ($section !== 'omschrijving')— "Skip summary section itself"omschrijvingis assigned to$statisticson the line after the loop, so the loop never sees itmethod_exists($this->archiMateService, '...Optimized')Plus several
isset() && !== nullpairs and non-nullable-entity null tests.One scoped ignore
OrganizationSyncService'sif ($contactObject !== null)is provably true — the code a few lines above already dereferences$contactObjectunconditionally, which is the giveaway. Left in place because the block it wraps is 243 lines: removing theifis a pure re-indentation of a quarter of the method, a large review-hostile diff for zero behaviour change. The ignore comment says so and marks it for the next real edit to that method.Verification
phpstan0 ·phpcsclean ·phpmdcleanPHPUnit is deliberately not part of this evidence. The bootstrap requires a booted Nextcloud (
Class "OC_App" not found) and cannot run standalone. I confirmed this is pre-existing by stashing every change in this branch and re-running — byte-identical failure. CI runs the suite inside the container.