feat(OCP): Consumable vs. Implementable public API

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/53072/head
Joas Schilling 11 months ago
parent bbc7041c07
commit d717dd9850
No known key found for this signature in database
GPG Key ID: F72FA5B49FFA96B0
  1. 92
      build/psalm/OcpSinceChecker.php
  2. 8
      lib/composer/composer/autoload_classmap.php
  3. 8
      lib/composer/composer/autoload_static.php
  4. 34
      lib/public/AppFramework/Attribute/ASince.php
  5. 23
      lib/public/AppFramework/Attribute/Catchable.php
  6. 27
      lib/public/AppFramework/Attribute/Consumable.php
  7. 23
      lib/public/AppFramework/Attribute/Dispatchable.php
  8. 38
      lib/public/AppFramework/Attribute/ExceptionalImplementable.php
  9. 27
      lib/public/AppFramework/Attribute/Implementable.php
  10. 23
      lib/public/AppFramework/Attribute/Listenable.php
  11. 23
      lib/public/AppFramework/Attribute/Throwable.php
  12. 8
      lib/public/Calendar/CalendarEventStatus.php
  13. 6
      lib/public/Notification/AlreadyProcessedException.php
  14. 8
      lib/public/Notification/IAction.php
  15. 8
      lib/public/Notification/IApp.php
  16. 8
      lib/public/Notification/IDeferrableApp.php
  17. 5
      lib/public/Notification/IDismissableNotifier.php
  18. 8
      lib/public/Notification/IManager.php
  19. 8
      lib/public/Notification/INotification.php
  20. 8
      lib/public/Notification/INotifier.php
  21. 5
      lib/public/Notification/IncompleteNotificationException.php
  22. 5
      lib/public/Notification/IncompleteParsedNotificationException.php
  23. 6
      lib/public/Notification/InvalidValueException.php
  24. 6
      lib/public/Notification/UnknownNotificationException.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,64 @@ 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(), [
'Catchable',
'Consumable',
'Dispatchable',
'Implementable',
'Listenable',
'Throwable',
], true)) {
$hasAppFrameworkAttribute = true;
self::checkAttributeHasValidSinceVersion($attr, $statementsSource);
}
if (in_array($attr->name->getLast(), [
'Catchable',
'Consumable',
'Listenable',
], true)) {
$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 +188,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)
)
);
}
}
}
}
}
}

@ -58,6 +58,14 @@ 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\\Catchable' => $baseDir . '/lib/public/AppFramework/Attribute/Catchable.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\\Listenable' => $baseDir . '/lib/public/AppFramework/Attribute/Listenable.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',

@ -99,6 +99,14 @@ 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\\Catchable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/Catchable.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\\Listenable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/Listenable.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',

@ -0,0 +1,34 @@
<?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;
/**
* Abstract base attribute to declare an API's stability.
*
* @since 32.0.0
*/
#[Consumable(since: '32.0.0')]
abstract class ASince {
/**
* @param string $since For shipped apps and server code such as core/ and lib/,
* this should be the server version. For other apps it
* should be the semantic app version.
*/
public function __construct(
protected string $since,
) {
}
public function getSince(): string {
return $this->since;
}
}

@ -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 exception is "catchable" by apps.
*
* @since 32.0.0
*/
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
#[Consumable(since: '32.0.0')]
#[Implementable(since: '32.0.0')]
class Catchable extends ASince {
}

@ -0,0 +1,27 @@
<?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.
*
* For events use @see \OCP\AppFramework\Attribute\Listenable
* For exceptions use @see \OCP\AppFramework\Attribute\Catchable
*
* @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 {
}

@ -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 {
}

@ -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/Listenable/Catchable
* has an exception and is Implementable/Dispatchable/Throwable by a dedicated
* 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 = null,
) {
}
public function getApp(): string {
return $this->app;
}
public function getClass(): ?string {
return $this->class;
}
}

@ -0,0 +1,27 @@
<?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.
*
* For events use @see \OCP\AppFramework\Attribute\Dispatchable
* For exceptions use @see \OCP\AppFramework\Attribute\Throwable
*
* @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 {
}

@ -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 "listenable" by apps.
*
* @since 32.0.0
*/
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
#[Consumable(since: '32.0.0')]
#[Implementable(since: '32.0.0')]
class Listenable extends ASince {
}

@ -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 exception is "throwable" by apps.
*
* @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 {
}

@ -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\Listenable;
#[Listenable(since: '32.0.0')]
enum CalendarEventStatus: string {
case TENTATIVE = 'TENTATIVE';
case CONFIRMED = 'CONFIRMED';

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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_]

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OCP\Notification;
use OCP\AppFramework\Attribute\Catchable;
/**
* 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
*/
#[Catchable(since: '30.0.0')]
class IncompleteNotificationException extends \InvalidArgumentException {
}

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OCP\Notification;
use OCP\AppFramework\Attribute\Catchable;
/**
* 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
*/
#[Catchable(since: '30.0.0')]
class IncompleteParsedNotificationException extends \InvalidArgumentException {
}

@ -9,9 +9,9 @@ declare(strict_types=1);
namespace OCP\Notification;
/**
* @since 30.0.0
*/
use OCP\AppFramework\Attribute\Catchable;
#[Catchable(since: '30.0.0')]
class InvalidValueException extends \InvalidArgumentException {
/**
* @since 30.0.0

@ -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 {
}

Loading…
Cancel
Save