aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoas Schilling <coding@schilljs.com>2025-05-23 11:19:12 +0200
committerJoas Schilling <coding@schilljs.com>2025-05-23 11:19:12 +0200
commit599807803685fa91c1a1faa7cdcb8bb6f2375acd (patch)
treeac78ca85c1cfef44f9584dbc0b63532373cf0c64
parent256b54858e6a5e833478b89629ed72f930990acf (diff)
downloadnextcloud-server-techdebt/standard-15/consumable-ocp.tar.gz
nextcloud-server-techdebt/standard-15/consumable-ocp.zip
feat(OCP): Consumable vs. Implementable public APItechdebt/standard-15/consumable-ocp
Signed-off-by: Joas Schilling <coding@schilljs.com>
-rw-r--r--build/psalm/OcpSinceChecker.php86
-rw-r--r--lib/composer/composer/autoload_classmap.php6
-rw-r--r--lib/composer/composer/autoload_static.php6
-rw-r--r--lib/public/AppFramework/Attribute/ASince.php30
-rw-r--r--lib/public/AppFramework/Attribute/Consumable.php25
-rw-r--r--lib/public/AppFramework/Attribute/Dispatchable.php23
-rw-r--r--lib/public/AppFramework/Attribute/ExceptionalImplementable.php38
-rw-r--r--lib/public/AppFramework/Attribute/Implementable.php24
-rw-r--r--lib/public/AppFramework/Attribute/Throwable.php24
-rw-r--r--lib/public/Calendar/CalendarEventStatus.php8
-rw-r--r--lib/public/Notification/AlreadyProcessedException.php6
-rw-r--r--lib/public/Notification/IAction.php8
-rw-r--r--lib/public/Notification/IApp.php8
-rw-r--r--lib/public/Notification/IDeferrableApp.php8
-rw-r--r--lib/public/Notification/IDismissableNotifier.php5
-rw-r--r--lib/public/Notification/IManager.php8
-rw-r--r--lib/public/Notification/INotification.php8
-rw-r--r--lib/public/Notification/INotifier.php8
-rw-r--r--lib/public/Notification/IncompleteNotificationException.php5
-rw-r--r--lib/public/Notification/IncompleteParsedNotificationException.php5
-rw-r--r--lib/public/Notification/InvalidValueException.php6
-rw-r--r--lib/public/Notification/UnknownNotificationException.php6
22 files changed, 299 insertions, 52 deletions
diff --git a/build/psalm/OcpSinceChecker.php b/build/psalm/OcpSinceChecker.php
index 959e70e0c4c..c33dd79367d 100644
--- a/build/psalm/OcpSinceChecker.php
+++ b/build/psalm/OcpSinceChecker.php
@@ -20,7 +20,18 @@ class OcpSinceChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitIn
$classLike = $event->getStmt();
$statementsSource = $event->getStatementsSource();
- self::checkClassComment($classLike, $statementsSource);
+ if (!str_contains($statementsSource->getFilePath(), '/lib/public/')) {
+ return;
+ }
+
+ $isTesting = str_contains($statementsSource->getFilePath(), '/lib/public/Notification/')
+ || str_contains($statementsSource->getFilePath(), 'CalendarEventStatus');
+
+ if ($isTesting) {
+ self::checkStatementAttributes($classLike, $statementsSource);
+ } else {
+ self::checkClassComment($classLike, $statementsSource);
+ }
foreach ($classLike->stmts as $stmt) {
if ($stmt instanceof ClassConst) {
@@ -32,11 +43,58 @@ class OcpSinceChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitIn
}
if ($stmt instanceof EnumCase) {
- self::checkStatementComment($stmt, $statementsSource, 'enum');
+ if ($isTesting) {
+ self::checkStatementAttributes($classLike, $statementsSource);
+ } else {
+ self::checkStatementComment($stmt, $statementsSource, 'enum');
+ }
}
}
}
+ private static function checkStatementAttributes(ClassLike $stmt, FileSource $statementsSource): void {
+ $hasAppFrameworkAttribute = false;
+ $mustBeConsumable = false;
+ $isConsumable = false;
+ foreach ($stmt->attrGroups as $attrGroup) {
+ foreach ($attrGroup->attrs as $attr) {
+ if (in_array($attr->name->getLast(), [
+ 'Consumable',
+ 'Dispatchable',
+ 'Implementable',
+ 'Throwable',
+ ], true)) {
+ $hasAppFrameworkAttribute = true;
+ self::checkAttributeHasValidSinceVersion($attr, $statementsSource);
+ }
+ if ($attr->name->getLast() === 'Consumable') {
+ $isConsumable = true;
+ }
+ if ($attr->name->getLast() === 'ExceptionalImplementable') {
+ $mustBeConsumable = true;
+ }
+ }
+ }
+
+ if ($mustBeConsumable && !$isConsumable) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'Attribute OCP\\AppFramework\\Attribute\\ExceptionalImplementable is only valid on classes that also have OCP\\AppFramework\\Attribute\\Consumable',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+
+ if (!$hasAppFrameworkAttribute) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'At least one of the OCP\\AppFramework\\Attribute attributes is required',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+ }
+
private static function checkClassComment(ClassLike $stmt, FileSource $statementsSource): void {
$docblock = $stmt->getDocComment();
@@ -124,4 +182,28 @@ class OcpSinceChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitIn
);
}
}
+
+ private static function checkAttributeHasValidSinceVersion(\PhpParser\Node\Attribute $stmt, FileSource $statementsSource): void {
+ foreach ($stmt->args as $arg) {
+ if ($arg->name?->name === 'since') {
+ if (!$arg->value instanceof \PhpParser\Node\Scalar\String_) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'Attribute since argument is not a valid version string',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ } else {
+ if (!preg_match('/^[1-9][0-9]*(\.[0-9]+){0,3}$/', $arg->value->value)) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'Attribute since argument is not a valid version string',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+ }
+ }
+ }
+ }
}
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index c65c26af2f2..6c525a923db 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -58,6 +58,12 @@ return array(
'OCP\\Activity\\ISetting' => $baseDir . '/lib/public/Activity/ISetting.php',
'OCP\\AppFramework\\ApiController' => $baseDir . '/lib/public/AppFramework/ApiController.php',
'OCP\\AppFramework\\App' => $baseDir . '/lib/public/AppFramework/App.php',
+ 'OCP\\AppFramework\\Attribute\\ASince' => $baseDir . '/lib/public/AppFramework/Attribute/ASince.php',
+ 'OCP\\AppFramework\\Attribute\\Consumable' => $baseDir . '/lib/public/AppFramework/Attribute/Consumable.php',
+ 'OCP\\AppFramework\\Attribute\\Dispatchable' => $baseDir . '/lib/public/AppFramework/Attribute/Dispatchable.php',
+ 'OCP\\AppFramework\\Attribute\\ExceptionalImplementable' => $baseDir . '/lib/public/AppFramework/Attribute/ExceptionalImplementable.php',
+ 'OCP\\AppFramework\\Attribute\\Implementable' => $baseDir . '/lib/public/AppFramework/Attribute/Implementable.php',
+ 'OCP\\AppFramework\\Attribute\\Throwable' => $baseDir . '/lib/public/AppFramework/Attribute/Throwable.php',
'OCP\\AppFramework\\AuthPublicShareController' => $baseDir . '/lib/public/AppFramework/AuthPublicShareController.php',
'OCP\\AppFramework\\Bootstrap\\IBootContext' => $baseDir . '/lib/public/AppFramework/Bootstrap/IBootContext.php',
'OCP\\AppFramework\\Bootstrap\\IBootstrap' => $baseDir . '/lib/public/AppFramework/Bootstrap/IBootstrap.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index b1e162bf71e..4e564979761 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -99,6 +99,12 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Activity\\ISetting' => __DIR__ . '/../../..' . '/lib/public/Activity/ISetting.php',
'OCP\\AppFramework\\ApiController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/ApiController.php',
'OCP\\AppFramework\\App' => __DIR__ . '/../../..' . '/lib/public/AppFramework/App.php',
+ 'OCP\\AppFramework\\Attribute\\ASince' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/ASince.php',
+ 'OCP\\AppFramework\\Attribute\\Consumable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/Consumable.php',
+ 'OCP\\AppFramework\\Attribute\\Dispatchable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/Dispatchable.php',
+ 'OCP\\AppFramework\\Attribute\\ExceptionalImplementable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/ExceptionalImplementable.php',
+ 'OCP\\AppFramework\\Attribute\\Implementable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/Implementable.php',
+ 'OCP\\AppFramework\\Attribute\\Throwable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/Throwable.php',
'OCP\\AppFramework\\AuthPublicShareController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/AuthPublicShareController.php',
'OCP\\AppFramework\\Bootstrap\\IBootContext' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Bootstrap/IBootContext.php',
'OCP\\AppFramework\\Bootstrap\\IBootstrap' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Bootstrap/IBootstrap.php',
diff --git a/lib/public/AppFramework/Attribute/ASince.php b/lib/public/AppFramework/Attribute/ASince.php
new file mode 100644
index 00000000000..266c43342f8
--- /dev/null
+++ b/lib/public/AppFramework/Attribute/ASince.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\AppFramework\Attribute;
+
+use Attribute;
+
+/**
+ * Attribute to declare that the API stability is limited to "implementing" the
+ * class, interface, enum, etc.
+ *
+ * @since 32.0.0
+ */
+#[Consumable(since: '32.0.0')]
+abstract class ASince {
+ public function __construct(
+ protected string $since,
+ ) {
+ }
+
+ public function getSince(): string {
+ return $this->since;
+ }
+}
diff --git a/lib/public/AppFramework/Attribute/Consumable.php b/lib/public/AppFramework/Attribute/Consumable.php
new file mode 100644
index 00000000000..f34a07f4d3c
--- /dev/null
+++ b/lib/public/AppFramework/Attribute/Consumable.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\AppFramework\Attribute;
+
+use Attribute;
+
+/**
+ * Attribute to declare that the API stability is limited to "consuming" the
+ * class, interface, enum, etc. Apps are not allowed to implement or replace them
+ * or in case of events dispatch them.
+ *
+ * @since 32.0.0
+ */
+#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
+#[Consumable(since: '32.0.0')]
+#[Implementable(since: '32.0.0')]
+class Consumable extends ASince {
+}
diff --git a/lib/public/AppFramework/Attribute/Dispatchable.php b/lib/public/AppFramework/Attribute/Dispatchable.php
new file mode 100644
index 00000000000..ff703d4749e
--- /dev/null
+++ b/lib/public/AppFramework/Attribute/Dispatchable.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\AppFramework\Attribute;
+
+use Attribute;
+
+/**
+ * Attribute to declare that the event is "dispatchable" by apps.
+ *
+ * @since 32.0.0
+ */
+#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
+#[Consumable(since: '32.0.0')]
+#[Implementable(since: '32.0.0')]
+class Dispatchable extends ASince {
+}
diff --git a/lib/public/AppFramework/Attribute/ExceptionalImplementable.php b/lib/public/AppFramework/Attribute/ExceptionalImplementable.php
new file mode 100644
index 00000000000..6c624bf8720
--- /dev/null
+++ b/lib/public/AppFramework/Attribute/ExceptionalImplementable.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\AppFramework\Attribute;
+
+use Attribute;
+
+/**
+ * Attribute to declare that the API marked as Consumable as an exception and is
+ * Implementable by a dedicated class in an app.
+ * Changes to such an API have to be communicated to the affected app maintainers.
+ *
+ * @since 32.0.0
+ */
+#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
+#[Consumable(since: '32.0.0')]
+#[Implementable(since: '32.0.0')]
+class ExceptionalImplementable {
+ public function __construct(
+ protected string $app,
+ protected string $class,
+ ) {
+ }
+
+ public function getApp(): string {
+ return $this->app;
+ }
+
+ public function getClass(): string {
+ return $this->class;
+ }
+}
diff --git a/lib/public/AppFramework/Attribute/Implementable.php b/lib/public/AppFramework/Attribute/Implementable.php
new file mode 100644
index 00000000000..154f2e91b7a
--- /dev/null
+++ b/lib/public/AppFramework/Attribute/Implementable.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\AppFramework\Attribute;
+
+use Attribute;
+
+/**
+ * Attribute to declare that the API stability is limited to "implementing" the
+ * class, interface, enum, etc.
+ *
+ * @since 32.0.0
+ */
+#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
+#[Consumable(since: '32.0.0')]
+#[Implementable(since: '32.0.0')]
+class Implementable extends ASince {
+}
diff --git a/lib/public/AppFramework/Attribute/Throwable.php b/lib/public/AppFramework/Attribute/Throwable.php
new file mode 100644
index 00000000000..603060455ad
--- /dev/null
+++ b/lib/public/AppFramework/Attribute/Throwable.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\AppFramework\Attribute;
+
+use Attribute;
+
+/**
+ * Attribute to declare that the API stability is limited to "throwing" the
+ * exception.
+ *
+ * @since 32.0.0
+ */
+#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
+#[Consumable(since: '32.0.0')]
+#[Implementable(since: '32.0.0')]
+class Throwable extends ASince {
+}
diff --git a/lib/public/Calendar/CalendarEventStatus.php b/lib/public/Calendar/CalendarEventStatus.php
index 5e070545758..d0b33712dad 100644
--- a/lib/public/Calendar/CalendarEventStatus.php
+++ b/lib/public/Calendar/CalendarEventStatus.php
@@ -7,11 +7,9 @@ declare(strict_types=1);
*/
namespace OCP\Calendar;
-/**
- * The status of a calendar event.
- *
- * @since 32.0.0
- */
+use OCP\AppFramework\Attribute\Consumable;
+
+#[Consumable(since: '32.0.0')]
enum CalendarEventStatus: string {
case TENTATIVE = 'TENTATIVE';
case CONFIRMED = 'CONFIRMED';
diff --git a/lib/public/Notification/AlreadyProcessedException.php b/lib/public/Notification/AlreadyProcessedException.php
index 0e7458185ff..162abd81864 100644
--- a/lib/public/Notification/AlreadyProcessedException.php
+++ b/lib/public/Notification/AlreadyProcessedException.php
@@ -8,9 +8,9 @@ declare(strict_types=1);
*/
namespace OCP\Notification;
-/**
- * @since 17.0.0
- */
+use OCP\AppFramework\Attribute\Throwable;
+
+#[Throwable(since: '17.0.0')]
class AlreadyProcessedException extends \RuntimeException {
/**
* @since 17.0.0
diff --git a/lib/public/Notification/IAction.php b/lib/public/Notification/IAction.php
index f1cffa49075..722dac72826 100644
--- a/lib/public/Notification/IAction.php
+++ b/lib/public/Notification/IAction.php
@@ -8,11 +8,9 @@ declare(strict_types=1);
*/
namespace OCP\Notification;
-/**
- * Interface IAction
- *
- * @since 9.0.0
- */
+use OCP\AppFramework\Attribute\Consumable;
+
+#[Consumable(since: '9.0.0')]
interface IAction {
/**
* @since 17.0.0
diff --git a/lib/public/Notification/IApp.php b/lib/public/Notification/IApp.php
index 1574ae8a091..37c352d44cd 100644
--- a/lib/public/Notification/IApp.php
+++ b/lib/public/Notification/IApp.php
@@ -8,11 +8,9 @@ declare(strict_types=1);
*/
namespace OCP\Notification;
-/**
- * Interface IApp
- *
- * @since 9.0.0
- */
+use OCP\AppFramework\Attribute\Implementable;
+
+#[Implementable(since: '9.0.0')]
interface IApp {
/**
* @param INotification $notification
diff --git a/lib/public/Notification/IDeferrableApp.php b/lib/public/Notification/IDeferrableApp.php
index 1820ed7ecd6..00c7d691b10 100644
--- a/lib/public/Notification/IDeferrableApp.php
+++ b/lib/public/Notification/IDeferrableApp.php
@@ -8,11 +8,9 @@ declare(strict_types=1);
*/
namespace OCP\Notification;
-/**
- * Interface IDeferrableApp
- *
- * @since 20.0.0
- */
+use OCP\AppFramework\Attribute\Implementable;
+
+#[Implementable(since: '20.0.0')]
interface IDeferrableApp extends IApp {
/**
* Start deferring notifications until `flush()` is called
diff --git a/lib/public/Notification/IDismissableNotifier.php b/lib/public/Notification/IDismissableNotifier.php
index 39f9658a8c4..d2f649b45a1 100644
--- a/lib/public/Notification/IDismissableNotifier.php
+++ b/lib/public/Notification/IDismissableNotifier.php
@@ -8,15 +8,16 @@ declare(strict_types=1);
*/
namespace OCP\Notification;
+use OCP\AppFramework\Attribute\Implementable;
+
/**
* Interface INotifier classes should implement if they want to process notifications
* that are dismissed by the user.
*
* This can be useful if dismissing the notification will leave it in an incomplete
* state. The handler can choose to for example do some default action.
- *
- * @since 18.0.0
*/
+#[Implementable(since: '18.0.0')]
interface IDismissableNotifier extends INotifier {
/**
* @param INotification $notification
diff --git a/lib/public/Notification/IManager.php b/lib/public/Notification/IManager.php
index 96427ddff92..23664af17cd 100644
--- a/lib/public/Notification/IManager.php
+++ b/lib/public/Notification/IManager.php
@@ -8,11 +8,9 @@ declare(strict_types=1);
*/
namespace OCP\Notification;
-/**
- * Interface IManager
- *
- * @since 9.0.0
- */
+use OCP\AppFramework\Attribute\Consumable;
+
+#[Consumable(since: '9.0.0')]
interface IManager extends IApp, INotifier {
/**
* @param string $appClass The service must implement IApp, otherwise a
diff --git a/lib/public/Notification/INotification.php b/lib/public/Notification/INotification.php
index 7a1ee960b28..a740678376f 100644
--- a/lib/public/Notification/INotification.php
+++ b/lib/public/Notification/INotification.php
@@ -8,11 +8,9 @@ declare(strict_types=1);
*/
namespace OCP\Notification;
-/**
- * Interface INotification
- *
- * @since 9.0.0
- */
+use OCP\AppFramework\Attribute\Consumable;
+
+#[Consumable(since: '9.0.0')]
interface INotification {
/**
* @param string $app
diff --git a/lib/public/Notification/INotifier.php b/lib/public/Notification/INotifier.php
index 39a962b0392..bdc7207216f 100644
--- a/lib/public/Notification/INotifier.php
+++ b/lib/public/Notification/INotifier.php
@@ -8,11 +8,9 @@ declare(strict_types=1);
*/
namespace OCP\Notification;
-/**
- * Interface INotifier
- *
- * @since 9.0.0
- */
+use OCP\AppFramework\Attribute\Implementable;
+
+#[Implementable(since: '9.0.0')]
interface INotifier {
/**
* Identifier of the notifier, only use [a-z0-9_]
diff --git a/lib/public/Notification/IncompleteNotificationException.php b/lib/public/Notification/IncompleteNotificationException.php
index f5ae5254509..0b537777134 100644
--- a/lib/public/Notification/IncompleteNotificationException.php
+++ b/lib/public/Notification/IncompleteNotificationException.php
@@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OCP\Notification;
+use OCP\AppFramework\Attribute\Throwable;
+
/**
* Thrown when {@see \OCP\Notification\IManager::notify()} is called with a notification
* that does not have all required fields set:
@@ -19,8 +21,7 @@ namespace OCP\Notification;
* - objectType
* - objectId
* - subject
- *
- * @since 30.0.0
*/
+#[Throwable(since: '30.0.0')]
class IncompleteNotificationException extends \InvalidArgumentException {
}
diff --git a/lib/public/Notification/IncompleteParsedNotificationException.php b/lib/public/Notification/IncompleteParsedNotificationException.php
index b69967e2781..1e12a7bdd7c 100644
--- a/lib/public/Notification/IncompleteParsedNotificationException.php
+++ b/lib/public/Notification/IncompleteParsedNotificationException.php
@@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OCP\Notification;
+use OCP\AppFramework\Attribute\Throwable;
+
/**
* Thrown when {@see \OCP\Notification\IManager::prepare()} is called with a notification
* that does not have all required fields set at the end of the manager or after a INotifier
@@ -22,8 +24,7 @@ namespace OCP\Notification;
* - objectType
* - objectId
* - parsedSubject
- *
- * @since 30.0.0
*/
+#[Throwable(since: '30.0.0')]
class IncompleteParsedNotificationException extends \InvalidArgumentException {
}
diff --git a/lib/public/Notification/InvalidValueException.php b/lib/public/Notification/InvalidValueException.php
index 05cf1a253b2..089724c3e42 100644
--- a/lib/public/Notification/InvalidValueException.php
+++ b/lib/public/Notification/InvalidValueException.php
@@ -9,9 +9,9 @@ declare(strict_types=1);
namespace OCP\Notification;
-/**
- * @since 30.0.0
- */
+use OCP\AppFramework\Attribute\Throwable;
+
+#[Throwable(since: '30.0.0')]
class InvalidValueException extends \InvalidArgumentException {
/**
* @since 30.0.0
diff --git a/lib/public/Notification/UnknownNotificationException.php b/lib/public/Notification/UnknownNotificationException.php
index 7e630c59dd0..976d9179592 100644
--- a/lib/public/Notification/UnknownNotificationException.php
+++ b/lib/public/Notification/UnknownNotificationException.php
@@ -9,8 +9,8 @@ declare(strict_types=1);
namespace OCP\Notification;
-/**
- * @since 30.0.0
- */
+use OCP\AppFramework\Attribute\Throwable;
+
+#[Throwable(since: '30.0.0')]
class UnknownNotificationException extends \InvalidArgumentException {
}