Skip to content

Commit c6a9bb1

Browse files
committed
feat(OCP): Consumable vs. Implementable public API
Signed-off-by: Joas Schilling <[email protected]>
1 parent bbc7041 commit c6a9bb1

24 files changed

+359
-52
lines changed

build/psalm/OcpSinceChecker.php

Lines changed: 90 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,64 @@ 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+
'Catchable',
63+
'Consumable',
64+
'Dispatchable',
65+
'Implementable',
66+
'Listenable',
67+
'Throwable',
68+
], true)) {
69+
$hasAppFrameworkAttribute = true;
70+
self::checkAttributeHasValidSinceVersion($attr, $statementsSource);
71+
}
72+
if (in_array($attr->name->getLast(), [
73+
'Catchable',
74+
'Consumable',
75+
'Listenable',
76+
], true)) {
77+
$isConsumable = true;
78+
}
79+
if ($attr->name->getLast() === 'ExceptionalImplementable') {
80+
$mustBeConsumable = true;
81+
}
82+
}
83+
}
84+
85+
if ($mustBeConsumable && !$isConsumable) {
86+
IssueBuffer::maybeAdd(
87+
new InvalidDocblock(
88+
'Attribute OCP\\AppFramework\\Attribute\\ExceptionalImplementable is only valid on classes that also have OCP\\AppFramework\\Attribute\\Consumable',
89+
new CodeLocation($statementsSource, $stmt)
90+
)
91+
);
92+
}
93+
94+
if (!$hasAppFrameworkAttribute) {
95+
IssueBuffer::maybeAdd(
96+
new InvalidDocblock(
97+
'At least one of the OCP\\AppFramework\\Attribute attributes is required',
98+
new CodeLocation($statementsSource, $stmt)
99+
)
100+
);
101+
}
102+
}
103+
40104
private static function checkClassComment(ClassLike $stmt, FileSource $statementsSource): void {
41105
$docblock = $stmt->getDocComment();
42106

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

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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
* Abstract base attribute to declare an API's stability.
16+
*
17+
* @since 32.0.0
18+
*/
19+
#[Consumable(since: '32.0.0')]
20+
abstract class ASince {
21+
/**
22+
* @param string $since For shipped apps and server code such as core/ and lib/,
23+
* this should be the server version. For other apps it
24+
* should be the semantic app version.
25+
*/
26+
public function __construct(
27+
protected string $since,
28+
) {
29+
}
30+
31+
public function getSince(): string {
32+
return $this->since;
33+
}
34+
}
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 exception is "catchable" 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 Catchable extends ASince {
23+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
*
18+
* For events use @see \OCP\AppFramework\Attribute\Listenable
19+
* For exceptions use @see \OCP\AppFramework\Attribute\Catchable
20+
*
21+
* @since 32.0.0
22+
*/
23+
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
24+
#[Consumable(since: '32.0.0')]
25+
#[Implementable(since: '32.0.0')]
26+
class Consumable extends ASince {
27+
}
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/Listenable/Catchable
16+
* has an exception and is Implementable/Dispatchable/Throwable by a dedicated
17+
* app. 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 = null,
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
* For events use @see \OCP\AppFramework\Attribute\Dispatchable
19+
* For exceptions use @see \OCP\AppFramework\Attribute\Throwable
20+
*
21+
* @since 32.0.0
22+
*/
23+
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
24+
#[Consumable(since: '32.0.0')]
25+
#[Implementable(since: '32.0.0')]
26+
class Implementable extends ASince {
27+
}
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 "listenable" 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 Listenable extends ASince {
23+
}

0 commit comments

Comments
 (0)