Skip to content

Commit 9c87c9c

Browse files
authored
fix(psalm): fix the real type problems, narrow the suppressions, replace the deprecated OCP calls (#1283)
Psalm: 389 errors when every suppression is lifted, down to 327, none of them a type or docblock problem. psalm.xml goes from 33 global suppressions to 2 (UnusedClass, PossiblyUnusedMethod, both DI); the rest are scoped per file with reasons, and UndefinedClass from 74 listed classes to 19. Deprecated OCP calls: all 20 from the audit plus IUserManager::search replaced with the APIs their deprecation notes name (isEnabledForAnyone, IUserConfig, IAppConfig, IResult, a pure SqlIdentifierQuoter on getDatabaseProvider, IBootContext::injectFn, searchDisplayName). No app id string was changed. New tests: PreferencesControllerTest, SqlIdentifierQuoterTest, a Health degraded-version test and an untitled-row warning test, each mutation-checked. Left: IAppManager::getInstalledApps in DemoDataService and SeedProfileService, because hydra gate 66 does not recognise getEnabledApps() as an OpenRegister availability guard. Refs #1277
1 parent e495393 commit 9c87c9c

56 files changed

Lines changed: 710 additions & 265 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

lib/AppInfo/Application.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,19 @@ public function boot(IBootContext $context): void {
197197
// case) without the full decidiq app bundle being present.
198198
Util::addInitScript(self::APP_ID, 'decidiq-integration-init');
199199

200-
$serverContainer = $context->getServerContainer();
201-
202200
// Object-lifecycle subscriptions MUST be made from boot(), never from
203201
// register(): OpenRegister's classes are only autoloadable to apps
204202
// registered after it, so the registrar's class_exists() guard would
205203
// resolve differently purely by app load order during register().
206-
$serverContainer->get(ObjectListenerRegistrar::class)->register(
207-
dispatcher: $serverContainer->get(IEventDispatcher::class)
204+
//
205+
// injectFn() rather than getServerContainer()->get(): IServerContainer
206+
// and IAppContainer are both deprecated since NC 20, and injectFn() is
207+
// the boot-time API that resolves the parameters from this app's
208+
// container (which falls back to the server for OCP services).
209+
$context->injectFn(
210+
static function (ObjectListenerRegistrar $registrar, IEventDispatcher $dispatcher): void {
211+
$registrar->register(dispatcher: $dispatcher);
212+
}
208213
);
209214

210215
}//end boot()

lib/BackgroundJob/TranscriptRetentionJob.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,12 @@ private function resolveBodyPolicy(object $objectService, array $meeting): array
270270
$body = $this->fetchObject(objectService: $objectService, id: (string)$bodyId, schema: 'governance-body');
271271
}
272272

273-
$policy = (string)(($body['transcriptRetentionPolicy'] ?? null) ?? self::DEFAULT_POLICY);
273+
$policy = (string)($body['transcriptRetentionPolicy'] ?? self::DEFAULT_POLICY);
274274
if (in_array($policy, self::POLICIES, true) === false) {
275275
$policy = self::DEFAULT_POLICY;
276276
}
277277

278-
$days = (int)(($body['transcriptRetentionDays'] ?? null) ?? self::DEFAULT_DAYS);
278+
$days = (int)($body['transcriptRetentionDays'] ?? self::DEFAULT_DAYS);
279279
if ($days < 0) {
280280
$days = self::DEFAULT_DAYS;
281281
}
@@ -501,7 +501,7 @@ private function resolveMeetingId(array $transcript): ?string {
501501
* @spec openspec/specs/meeting-transcription/spec.md
502502
*/
503503
private function objectId(array $object): ?string {
504-
$id = (string)(($object['id'] ?? ($object['@self']['id'] ?? null)) ?? '');
504+
$id = (string)($object['id'] ?? $object['@self']['id'] ?? '');
505505
if ($id === '') {
506506
return null;
507507
}

lib/Controller/ApprovalRouteController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ public function record(): JSONResponse {
153153
// A final signature given over THIS surface concludes the route just as
154154
// surely as one arriving over the cross-app seam, and the producer that
155155
// delegated its runtime here is waiting on the announcement.
156-
$this->announcer->announceIfConcluded(subject: (string)$action['subject']);
156+
$this->announcer->announceIfConcluded(subject: $action['subject']);
157157

158158
return new JSONResponse($recorded, Http::STATUS_CREATED);
159159
}//end record()

lib/Controller/HealthController.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
4848
use OCP\AppFramework\Http\Attribute\PublicPage;
4949
use OCP\AppFramework\Http\JSONResponse;
50+
use OCP\IAppConfig;
5051
use OCP\IConfig;
5152
use OCP\IRequest;
5253
use Psr\Container\ContainerInterface;
@@ -91,13 +92,15 @@ class HealthController extends Controller {
9192
* @param IRequest $request The request object.
9293
* @param IConfig $config The Nextcloud config service (baseUrl).
9394
* @param ContainerInterface $container DI container — resolves the AppHost engine lazily.
95+
* @param IAppConfig $appConfig App config store (installed version on the degraded path).
9496
*
9597
* @return void
9698
*/
9799
public function __construct(
98100
IRequest $request,
99101
private readonly IConfig $config,
100102
private readonly ContainerInterface $container,
103+
private readonly IAppConfig $appConfig,
101104
) {
102105
parent::__construct(appName: Application::APP_ID, request: $request);
103106

@@ -134,7 +137,7 @@ public function index(): JSONResponse {
134137
if ($body === null) {
135138
$body = [
136139
'status' => 'degraded',
137-
'version' => $this->config->getAppValue(Application::APP_ID, 'installed_version', ''),
140+
'version' => $this->appConfig->getValueString(Application::APP_ID, 'installed_version', ''),
138141
'openregister' => 'unavailable',
139142
'httpStatus' => Http::STATUS_OK,
140143
];

lib/Controller/LiveMeetingController.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,6 @@ public function recordLiveDecision(string $meetingId): JSONResponse {
125125
return $denied;
126126
}
127127

128-
$user = $this->userSession->getUser();
129-
130128
try {
131129
$title = $this->request->getParam('title');
132130
$text = $this->request->getParam('text');
@@ -148,8 +146,7 @@ public function recordLiveDecision(string $meetingId): JSONResponse {
148146

149147
$decisionSlug = $this->liveDecisionService->recordDecision(
150148
$meetingId,
151-
$decisionData,
152-
$user->getUID()
149+
$decisionData
153150
);
154151

155152
return new JSONResponse(

lib/Controller/MinutesController.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -347,13 +347,8 @@ public function submitForApproval(string $minutesId): JSONResponse {
347347
return $denied;
348348
}
349349

350-
$user = $this->userSession->getUser();
351-
352350
return $this->responder->runInternal(
353-
operation: fn (): array => $this->workflowService->submitForApproval(
354-
minutesId: $minutesId,
355-
actorId: $user->getUID()
356-
)
351+
operation: fn (): array => $this->workflowService->submitForApproval(minutesId: $minutesId)
357352
);
358353

359354
}//end submitForApproval()

lib/Controller/OriController.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,22 +353,22 @@ public function show(string $resource, string $id): JSONResponse {
353353
// (publicationDate <= $now, not depublished). A future-dated or
354354
// depublished payload is not-found for anonymous callers — return 404
355355
// (not 403) so the endpoint never confirms an unpublished payload exists.
356-
if ($this->serializer->isPayloadLive(object: (array)$object) === false) {
356+
if ($this->serializer->isPayloadLive(object: $object) === false) {
357357
return $this->errorResponse(message: 'Not found', status: Http::STATUS_NOT_FOUND);
358358
}
359359

360360
return $this->jsonLdResponse(
361-
payload: $this->serializer->serializePayload(object: (array)$object, fallbackType: $type)
361+
payload: $this->serializer->serializePayload(object: $object, fallbackType: $type)
362362
);
363363
}//end if
364364

365365
// #316: Treat non-published objects as not-found for anonymous callers.
366366
// Return 404 (not 403) to avoid confirming the object exists.
367-
if ($this->isLifecycleBlocked(object: (array)$object) === true) {
367+
if ($this->isLifecycleBlocked(object: $object) === true) {
368368
return $this->errorResponse(message: 'Not found', status: Http::STATUS_NOT_FOUND);
369369
}
370370

371-
return $this->jsonLdResponse(payload: $this->serializer->serialize(type: $type, object: (array)$object));
371+
return $this->jsonLdResponse(payload: $this->serializer->serialize(type: $type, object: $object));
372372
}//end show()
373373

374374
/**

lib/Controller/PreferencesController.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
/**
44
* Decidiq PreferencesController.
55
*
6-
* Generic per-user key/value preferences, backed by Nextcloud IConfig
7-
* user values. Used by shared @conduction/nextcloud-vue widgets (e.g.
6+
* Generic per-user key/value preferences, backed by Nextcloud's per-user
7+
* config store (IUserConfig). Used by shared @conduction/nextcloud-vue widgets (e.g.
88
* CnSupportDialog's "seen" flag) that need to persist a small per-user
99
* UI flag cross-device without a bespoke endpoint per feature.
1010
*
@@ -28,7 +28,7 @@
2828
use OCP\AppFramework\Controller;
2929
use OCP\AppFramework\Http;
3030
use OCP\AppFramework\Http\JSONResponse;
31-
use OCP\IConfig;
31+
use OCP\Config\IUserConfig;
3232
use OCP\IRequest;
3333
use OCP\IUserSession;
3434

@@ -42,12 +42,12 @@ class PreferencesController extends Controller {
4242
* Constructor.
4343
*
4444
* @param IRequest $request The request.
45-
* @param IConfig $config The Nextcloud config (user values).
45+
* @param IUserConfig $userConfig The Nextcloud per-user config store.
4646
* @param IUserSession $userSession The user session.
4747
*/
4848
public function __construct(
4949
IRequest $request,
50-
private readonly IConfig $config,
50+
private readonly IUserConfig $userConfig,
5151
private readonly IUserSession $userSession,
5252
) {
5353
parent::__construct(appName: Application::APP_ID, request: $request);
@@ -77,9 +77,9 @@ public function getPreference(string $key): JSONResponse {
7777
return new JSONResponse(data: ['message' => 'Invalid key'], statusCode: Http::STATUS_BAD_REQUEST);
7878
}
7979

80-
$value = $this->config->getUserValue(
80+
$value = $this->userConfig->getValueString(
8181
userId: $user->getUID(),
82-
appName: Application::APP_ID,
82+
app: Application::APP_ID,
8383
key: 'pref_' . $safeKey,
8484
default: ''
8585
);
@@ -118,18 +118,18 @@ public function setPreference(string $key, string $value = ''): JSONResponse {
118118

119119
// An empty value clears the preference rather than storing a blank one.
120120
if ($value === '') {
121-
$this->config->deleteUserValue(
121+
$this->userConfig->deleteUserConfig(
122122
userId: $user->getUID(),
123-
appName: Application::APP_ID,
123+
app: Application::APP_ID,
124124
key: 'pref_' . $safeKey
125125
);
126126

127127
return new JSONResponse(data: ['value' => null]);
128128
}
129129

130-
$this->config->setUserValue(
130+
$this->userConfig->setValueString(
131131
userId: $user->getUID(),
132-
appName: Application::APP_ID,
132+
app: Application::APP_ID,
133133
key: 'pref_' . $safeKey,
134134
value: $value
135135
);
@@ -139,7 +139,7 @@ public function setPreference(string $key, string $value = ''): JSONResponse {
139139

140140
/**
141141
* Restrict keys to a safe charset so callers cannot reach arbitrary
142-
* IConfig user values outside the `pref_` namespace.
142+
* per-user config values outside the `pref_` namespace.
143143
*
144144
* @param string $key The raw key.
145145
*

lib/Dashboard/DecidiqDashboardWidget.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ public function load(): void {
163163
*
164164
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
165165
*
166-
* @return WidgetButton[] The widget buttons
166+
* @return list<WidgetButton> The widget buttons
167167
*/
168168
public function getWidgetButtons(string $userId): array {
169169
return [

lib/Lifecycle/QesGuard.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ private function loadSignedBy(string $resolutionId): array {
207207
]
208208
);
209209

210-
foreach ((array)$minutesRows as $row) {
210+
foreach ($minutesRows as $row) {
211211
$minutes = $this->toArray(row: $row);
212212
if (($minutes['meetingIntegration'] ?? null) !== $meetingId) {
213213
continue;

0 commit comments

Comments
 (0)