fix(psalm): fix the real type problems, narrow the suppressions, replace the deprecated OCP calls - #1283
Merged
Merged
Conversation
Lifting the 33 globally suppressed psalm issue types produced 389 errors.
About 296 are unused-code reports from DI wiring; the rest were real.
Fixed:
- `($a ?? null) ?? $d` on expressions (TranscriptRetentionJob x3,
MigrateConsultationsToOneSchema, MigrateQuestionsToAgendaItems). In the
last one the outer `??` guarded nothing, so a row with neither a question
nor a request number raised "Undefined array key" on every upgrade. New
test testAnUntitledRowSavesWithoutAWarning fails on the old code.
- Wrong return docblocks: DecisionIntegrationService::registerOutcomeCallback
(decisionId, callbackUrl), SeedProfileService::listProfiles (icon),
DecidiqDashboardWidget, DecidiqSearchProvider.
- ParticipantNotifier: guard the container-resolved IUserManager.
- 7 docblocks whose `{...}` descriptions psalm read as types.
- 32 casts and 1 array_values() psalm proves redundant from native types,
and one always-true condition in MinutesDraftComposer.
- Removed the three "reserved for future" parameters: recordDecision
$actorId, notifyApproversOnSubmit $actorId (and submitForApproval, which
only passed it on), OpenCatalogiPublisher::retract $catalogId.
psalm.xml: 33 global suppressions down to 2 (UnusedClass,
PossiblyUnusedMethod, both DI). Everything else is scoped to the file or
class that needs it, each with its reason. UndefinedClass narrowed from 74
referenced classes to the 19 psalm reports.
Refs #1277
All 20 deprecated calls from the #1277 audit, plus IUserManager::search, which psalm could only see once ParticipantNotifier typed its user manager. Each replacement is the one the OCP deprecation note names, and each was checked against the server implementation for the same behaviour. - IAppManager::isInstalled (6): isEnabledForAnyone. AppManager::isInstalled is literally `return $this->isEnabledForAnyone($appId)`. App id strings unchanged. - IConfig user values (PreferencesController, MigrateUserPreferences): IUserConfig getValueString / setValueString / deleteUserConfig / getKeys. New PreferencesControllerTest (6 tests). - IConfig::getAppValue (HealthController degraded body): IAppConfig getValueString. New test for the degraded version. - IPreparedStatement::fetch/fetchAll (4): read from the IResult execute() returns, then close the cursor. - IDBConnection::getDatabasePlatform (2): new pure SqlIdentifierQuoter keyed on getDatabaseProvider(). Compared against the Doctrine platforms Nextcloud ships (MySQL80, MariaDB, PostgreSQL, SQLite, Oracle): 30 of 30 identical. - IServerContainer in Application::boot: IBootContext::injectFn(). - IUserManager::search: searchDisplayName, which is what the docblock says that step does. Left: IAppManager::getInstalledApps in DemoDataService and SeedProfileService. Its replacement getEnabledApps() is not recognised by hydra gate 66 as an OpenRegister availability guard, so switching fails the gate. Refs #1277
rubenvdlinde
requested review from
Rem-Dam,
SudoThijn,
WilcoLouwerse,
bbrands02,
remko48 and
rjzondervan
as code owners
September 11, 2026 07:14
Contributor
Quality Report — ConductionNL/decidiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-nav-ceiling | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 104/104 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-11 07:46 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.
Second W1a static-analysis PR for #1277, on top of #1278.
Psalm
Lifting the 33 globally suppressed issue types produced 389 errors: about 296 unused-code reports caused by DI wiring, the rest real. After this PR the same measurement is 327, and everything in it is either DI noise or a documented false positive. Psalm exits 0 and reports no deprecations at all.
Fixed:
($a ?? null) ?? $defaultwritten on an expression, where??cannot do its isset job:TranscriptRetentionJob(3),MigrateConsultationsToOneSchema,MigrateQuestionsToAgendaItems. In the last one the guard was load-bearing: a question row naming neither a question nor a request number raisedUndefined array key "requestNumber"on every upgrade.testAnUntitledRowSavesWithoutAWarningfails on the old code with exactly that string.DecisionIntegrationService::registerOutcomeCallback(returnsdecisionIdandcallbackUrltoo),SeedProfileService::listProfiles(always returnsicon),DecidiqDashboardWidget::getWidgetButtons,DecidiqSearchProvider::getOrder.ParticipantNotifier::resolveUidnow checks the container-resolved user manager is anIUserManagerbefore calling it, so psalm can type the two calls it could not.{...}key lists psalm parsed as part of the type, which made it drop the whole docblock.array_values()that psalm proves redundant from native types, plus one always-true condition inMinutesDraftComposer.LiveDecisionService::recordDecision($actorId),MinutesService::notifyApproversOnSubmit($actorId)(andMinutesWorkflowService::submitForApproval, whose only use of it was passing it on),OpenCatalogiPublisher::retract($catalogId). Callers and tests follow.psalm.xmlnow suppresses 2 issue types globally instead of 33:UnusedClassandPossiblyUnusedMethod, which are what DI wiring looks like to a static analyser (73 and 223 reports). Everything else is scoped to the file that needs it with the reason written next to it, and each scope was probed by removing it and re-running.UndefinedClasswent from 74 listed classes to the 19 psalm actually reports.Deprecated OCP calls
All 20 from the audit, plus
IUserManager::search, which only became visible once the user manager was typed. Each replacement is the one the OCP deprecation note names, and each was checked against the server implementation:IAppManager::isInstalled(6)isEnabledForAnyoneAppManager::isInstalled()isreturn $this->isEnabledForAnyone($appId)IConfig::get/set/deleteUserValue,getUserKeysIUserConfigAllConfigdelegates to exactly theseIConfig::getAppValueIAppConfig::getValueStringIPreparedStatement::fetch/fetchAll(4)IResultthatexecute()returnsIDBConnection::getDatabasePlatform(2)SqlIdentifierQuoterongetDatabaseProvider()quoteSingleIdentifier()on the five platforms Nextcloud ships: 30 of 30 identicalIServerContainerinboot()IBootContext::injectFn()IAppContaineris deprecated too, so the container is not named at allIUserManager::searchsearchDisplayNameNo app id string was changed. The fleet rename makes those duck-typed lookups, so only the API moved.
New tests:
PreferencesControllerTest(6),SqlIdentifierQuoterTest(8), the Health degraded-version test, and the untitled-row warning test. Every one was mutation-checked: broken, watched the right assertion redden, restored,diffagainst a backup.Left deliberately:
IAppManager::getInstalledAppsinDemoDataServiceandSeedProfileService. The replacement isgetEnabledApps(), but hydra gate 66 (openregister-dependency-shape) recognises onlyisInstalled('openregister'),isEnabledForUser,getInstalledApps(andclass_existsas an availability guard, so the non-deprecated spelling turns a guarded lookup into 2 FAILs. That is a gate gap for the whole fleet rather than something to fix per app: gate 66's_AVAILABILITY_REneedsisEnabledForAnyone('openregister')andgetEnabledApps(.Verification (local, on this branch, after merging development)
composer check:strictexit 0: lint, phpcs (0 warnings), phpmd, psalm, phpstan, 1507 tests../vendor/bin/hydra-gatesexit 0: 87 of 87 applicable gates ran and passed.--show-info: 0 deprecation reports.Refs #1277
🤖 Generated with Claude Code