Skip to content

Commit 5998078

Browse files
committed
feat(OCP): Consumable vs. Implementable public API
Signed-off-by: Joas Schilling <coding@schilljs.com>
1 parent 256b548 commit 5998078

22 files changed

Lines changed: 299 additions & 52 deletions

build/psalm/OcpSinceChecker.php

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): voi
2020
$classLike = $event->getStmt();
2121
$statementsSource = $event->getStatementsSource();
2222

23-
self::checkClassComment($classLike, $statementsSource);
23+
if (!str_contains($statementsSource->getFilePath(), '/lib/public/')) {
24+
return;
25+
}
26+
27+
$isTesting = str_contains($statementsSource->getFilePath(), '/lib/public/Notification/')
28+
|| str_contains($statementsSource->getFilePath(), 'CalendarEventStatus');
29+
30+
if ($isTesting) {
31+
self::checkStatementAttributes($classLike, $statementsSource);
32+
} else {
33+
self::checkClassComment($classLike, $statementsSource);
34+
}
2435

2536
foreach ($classLike->stmts as $stmt) {
2637
if ($stmt instanceof ClassConst) {
@@ -32,11 +43,58 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): voi
3243
}
3344

3445
if ($stmt instanceof EnumCase) {
35-
self::checkStatementComment($stmt, $statementsSource, 'enum');
46+
if ($isTesting) {
47+
self::checkStatementAttributes($classLike, $statementsSource);
48+
} else {
49+
self::checkStatementComment($stmt, $statementsSource, 'enum');
50+
}
3651
}
3752
}
3853
}
3954

55+
private static function checkStatementAttributes(ClassLike $stmt, FileSource $statementsSource): void {
56+
$hasAppFrameworkAttribute = false;
57+
$mustBeConsumable = false;
58+
$isConsumable = false;
59+
foreach ($stmt->attrGroups as $attrGroup) {
60+
foreach ($attrGroup->attrs as $attr) {
61+
if (in_array($attr->name->getLast(), [
62+
'Consumable',
63+
'Dispatchable',
64+
'Implementable',
65+
'Throwable',
66+
], true)) {
67+
$hasAppFrameworkAttribute = true;
68+
self::checkAttributeHasValidSinceVersion($attr, $statementsSource);
69+
}
70+
if ($attr->name->getLast() === 'Consumable') {
71+
$isConsumable = true;
72+
}
73+
if ($attr->name->getLast() === 'ExceptionalImplementable') {
74+
$mustBeConsumable = true;
75+
}
76+
}
77+
}
78+
79+
if ($mustBeConsumable && !$isConsumable) {
80+
IssueBuffer::maybeAdd(
81+
new InvalidDocblock(
82+
'Attribute OCP\\AppFramework\\Attribute\\ExceptionalImplementable is only valid on classes that also have OCP\\AppFramework\\Attribute\\Consumable',
83+
new CodeLocation($statementsSource, $stmt)
84+
)
85+
);
86+
}
87+
88+
if (!$hasAppFrameworkAttribute) {
89+
IssueBuffer::maybeAdd(
90+
new InvalidDocblock(
91+
'At least one of the OCP\\AppFramework\\Attribute attributes is required',
92+
new CodeLocation($statementsSource, $stmt)
93+
)
94+
);
95+
}
96+
}
97+
4098
private static function checkClassComment(ClassLike $stmt, FileSource $statementsSource): void {
4199
$docblock = $stmt->getDocComment();
42100

@@ -124,4 +182,28 @@ private static function checkStatementComment(Stmt $stmt, FileSource $statements
124182
);
125183
}
126184
}
185+
186+
private static function checkAttributeHasValidSinceVersion(\PhpParser\Node\Attribute $stmt, FileSource $statementsSource): void {
187+
foreach ($stmt->args as $arg) {
188+
if ($arg->name?->name === 'since') {
189+
if (!$arg->value instanceof \PhpParser\Node\Scalar\String_) {
190+
IssueBuffer::maybeAdd(
191+
new InvalidDocblock(
192+
'Attribute since argument is not a valid version string',
193+
new CodeLocation($statementsSource, $stmt)
194+
)
195+
);
196+
} else {
197+
if (!preg_match('/^[1-9][0-9]*(\.[0-9]+){0,3}$/', $arg->value->value)) {
198+
IssueBuffer::maybeAdd(
199+
new InvalidDocblock(
200+
'Attribute since argument is not a valid version string',
201+
new CodeLocation($statementsSource, $stmt)
202+
)
203+
);
204+
}
205+
}
206+
}
207+
}
208+
}
127209
}

lib/composer/composer/autoload_classmap.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@
5858
'OCP\\Activity\\ISetting' => $baseDir . '/lib/public/Activity/ISetting.php',
5959
'OCP\\AppFramework\\ApiController' => $baseDir . '/lib/public/AppFramework/ApiController.php',
6060
'OCP\\AppFramework\\App' => $baseDir . '/lib/public/AppFramework/App.php',
61+
'OCP\\AppFramework\\Attribute\\ASince' => $baseDir . '/lib/public/AppFramework/Attribute/ASince.php',
62+
'OCP\\AppFramework\\Attribute\\Consumable' => $baseDir . '/lib/public/AppFramework/Attribute/Consumable.php',
63+
'OCP\\AppFramework\\Attribute\\Dispatchable' => $baseDir . '/lib/public/AppFramework/Attribute/Dispatchable.php',
64+
'OCP\\AppFramework\\Attribute\\ExceptionalImplementable' => $baseDir . '/lib/public/AppFramework/Attribute/ExceptionalImplementable.php',
65+
'OCP\\AppFramework\\Attribute\\Implementable' => $baseDir . '/lib/public/AppFramework/Attribute/Implementable.php',
66+
'OCP\\AppFramework\\Attribute\\Throwable' => $baseDir . '/lib/public/AppFramework/Attribute/Throwable.php',
6167
'OCP\\AppFramework\\AuthPublicShareController' => $baseDir . '/lib/public/AppFramework/AuthPublicShareController.php',
6268
'OCP\\AppFramework\\Bootstrap\\IBootContext' => $baseDir . '/lib/public/AppFramework/Bootstrap/IBootContext.php',
6369
'OCP\\AppFramework\\Bootstrap\\IBootstrap' => $baseDir . '/lib/public/AppFramework/Bootstrap/IBootstrap.php',

lib/composer/composer/autoload_static.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
9999
'OCP\\Activity\\ISetting' => __DIR__ . '/../../..' . '/lib/public/Activity/ISetting.php',
100100
'OCP\\AppFramework\\ApiController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/ApiController.php',
101101
'OCP\\AppFramework\\App' => __DIR__ . '/../../..' . '/lib/public/AppFramework/App.php',
102+
'OCP\\AppFramework\\Attribute\\ASince' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/ASince.php',
103+
'OCP\\AppFramework\\Attribute\\Consumable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/Consumable.php',
104+
'OCP\\AppFramework\\Attribute\\Dispatchable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/Dispatchable.php',
105+
'OCP\\AppFramework\\Attribute\\ExceptionalImplementable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/ExceptionalImplementable.php',
106+
'OCP\\AppFramework\\Attribute\\Implementable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/Implementable.php',
107+
'OCP\\AppFramework\\Attribute\\Throwable' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Attribute/Throwable.php',
102108
'OCP\\AppFramework\\AuthPublicShareController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/AuthPublicShareController.php',
103109
'OCP\\AppFramework\\Bootstrap\\IBootContext' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Bootstrap/IBootContext.php',
104110
'OCP\\AppFramework\\Bootstrap\\IBootstrap' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Bootstrap/IBootstrap.php',
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\AppFramework\Attribute;
11+
12+
use Attribute;
13+
14+
/**
15+
* Attribute to declare that the API stability is limited to "implementing" the
16+
* class, interface, enum, etc.
17+
*
18+
* @since 32.0.0
19+
*/
20+
#[Consumable(since: '32.0.0')]
21+
abstract class ASince {
22+
public function __construct(
23+
protected string $since,
24+
) {
25+
}
26+
27+
public function getSince(): string {
28+
return $this->since;
29+
}
30+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\AppFramework\Attribute;
11+
12+
use Attribute;
13+
14+
/**
15+
* Attribute to declare that the API stability is limited to "consuming" the
16+
* class, interface, enum, etc. Apps are not allowed to implement or replace them
17+
* or in case of events dispatch them.
18+
*
19+
* @since 32.0.0
20+
*/
21+
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
22+
#[Consumable(since: '32.0.0')]
23+
#[Implementable(since: '32.0.0')]
24+
class Consumable extends ASince {
25+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\AppFramework\Attribute;
11+
12+
use Attribute;
13+
14+
/**
15+
* Attribute to declare that the event is "dispatchable" by apps.
16+
*
17+
* @since 32.0.0
18+
*/
19+
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
20+
#[Consumable(since: '32.0.0')]
21+
#[Implementable(since: '32.0.0')]
22+
class Dispatchable extends ASince {
23+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\AppFramework\Attribute;
11+
12+
use Attribute;
13+
14+
/**
15+
* Attribute to declare that the API marked as Consumable as an exception and is
16+
* Implementable by a dedicated class in an app.
17+
* Changes to such an API have to be communicated to the affected app maintainers.
18+
*
19+
* @since 32.0.0
20+
*/
21+
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
22+
#[Consumable(since: '32.0.0')]
23+
#[Implementable(since: '32.0.0')]
24+
class ExceptionalImplementable {
25+
public function __construct(
26+
protected string $app,
27+
protected string $class,
28+
) {
29+
}
30+
31+
public function getApp(): string {
32+
return $this->app;
33+
}
34+
35+
public function getClass(): string {
36+
return $this->class;
37+
}
38+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\AppFramework\Attribute;
11+
12+
use Attribute;
13+
14+
/**
15+
* Attribute to declare that the API stability is limited to "implementing" the
16+
* class, interface, enum, etc.
17+
*
18+
* @since 32.0.0
19+
*/
20+
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
21+
#[Consumable(since: '32.0.0')]
22+
#[Implementable(since: '32.0.0')]
23+
class Implementable extends ASince {
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\AppFramework\Attribute;
11+
12+
use Attribute;
13+
14+
/**
15+
* Attribute to declare that the API stability is limited to "throwing" the
16+
* exception.
17+
*
18+
* @since 32.0.0
19+
*/
20+
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
21+
#[Consumable(since: '32.0.0')]
22+
#[Implementable(since: '32.0.0')]
23+
class Throwable extends ASince {
24+
}

lib/public/Calendar/CalendarEventStatus.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@
77
*/
88
namespace OCP\Calendar;
99

10-
/**
11-
* The status of a calendar event.
12-
*
13-
* @since 32.0.0
14-
*/
10+
use OCP\AppFramework\Attribute\Consumable;
11+
12+
#[Consumable(since: '32.0.0')]
1513
enum CalendarEventStatus: string {
1614
case TENTATIVE = 'TENTATIVE';
1715
case CONFIRMED = 'CONFIRMED';

0 commit comments

Comments
 (0)