@@ -12,35 +12,35 @@ |
||
12 | 12 | |
13 | 13 | #[Implementable(since: '9.0.0')] |
14 | 14 | interface INotifier { |
15 | - /** |
|
16 | - * Identifier of the notifier, only use [a-z0-9_] |
|
17 | - * |
|
18 | - * @return string |
|
19 | - * @since 17.0.0 |
|
20 | - */ |
|
21 | - public function getID(): string; |
|
15 | + /** |
|
16 | + * Identifier of the notifier, only use [a-z0-9_] |
|
17 | + * |
|
18 | + * @return string |
|
19 | + * @since 17.0.0 |
|
20 | + */ |
|
21 | + public function getID(): string; |
|
22 | 22 | |
23 | - /** |
|
24 | - * Human-readable name describing the notifier |
|
25 | - * |
|
26 | - * @return string |
|
27 | - * @since 17.0.0 |
|
28 | - */ |
|
29 | - public function getName(): string; |
|
23 | + /** |
|
24 | + * Human-readable name describing the notifier |
|
25 | + * |
|
26 | + * @return string |
|
27 | + * @since 17.0.0 |
|
28 | + */ |
|
29 | + public function getName(): string; |
|
30 | 30 | |
31 | - /** |
|
32 | - * @param INotification $notification |
|
33 | - * @param string $languageCode The code of the language that should be used to prepare the notification |
|
34 | - * @return INotification |
|
35 | - * @throws UnknownNotificationException When the notification was not prepared by a notifier |
|
36 | - * @throws AlreadyProcessedException When the notification is not needed anymore and should be deleted |
|
37 | - * @throws IncompleteParsedNotificationException Only to be thrown by the {@see IManager} |
|
38 | - * @since 9.0.0 |
|
39 | - * @since 30.0.0 Notifiers should throw {@see UnknownNotificationException} instead of \InvalidArgumentException |
|
40 | - * when they did not handle the notification. Throwing \InvalidArgumentException directly is deprecated and will |
|
41 | - * be logged as an error in Nextcloud 39. |
|
42 | - * @since 30.0.0 Throws {@see IncompleteParsedNotificationException} when not all required fields |
|
43 | - * are set at the end of the manager or after a INotifier that claimed to have parsed the notification. |
|
44 | - */ |
|
45 | - public function prepare(INotification $notification, string $languageCode): INotification; |
|
31 | + /** |
|
32 | + * @param INotification $notification |
|
33 | + * @param string $languageCode The code of the language that should be used to prepare the notification |
|
34 | + * @return INotification |
|
35 | + * @throws UnknownNotificationException When the notification was not prepared by a notifier |
|
36 | + * @throws AlreadyProcessedException When the notification is not needed anymore and should be deleted |
|
37 | + * @throws IncompleteParsedNotificationException Only to be thrown by the {@see IManager} |
|
38 | + * @since 9.0.0 |
|
39 | + * @since 30.0.0 Notifiers should throw {@see UnknownNotificationException} instead of \InvalidArgumentException |
|
40 | + * when they did not handle the notification. Throwing \InvalidArgumentException directly is deprecated and will |
|
41 | + * be logged as an error in Nextcloud 39. |
|
42 | + * @since 30.0.0 Throws {@see IncompleteParsedNotificationException} when not all required fields |
|
43 | + * are set at the end of the manager or after a INotifier that claimed to have parsed the notification. |
|
44 | + */ |
|
45 | + public function prepare(INotification $notification, string $languageCode): INotification; |
|
46 | 46 | } |
@@ -11,7 +11,7 @@ |
||
11 | 11 | |
12 | 12 | #[Listenable(since: '32.0.0')] |
13 | 13 | enum CalendarEventStatus: string { |
14 | - case TENTATIVE = 'TENTATIVE'; |
|
15 | - case CONFIRMED = 'CONFIRMED'; |
|
16 | - case CANCELLED = 'CANCELLED'; |
|
14 | + case TENTATIVE = 'TENTATIVE'; |
|
15 | + case CONFIRMED = 'CONFIRMED'; |
|
16 | + case CANCELLED = 'CANCELLED'; |
|
17 | 17 | }; |
@@ -22,17 +22,17 @@ |
||
22 | 22 | #[Consumable(since: '32.0.0')] |
23 | 23 | #[Implementable(since: '32.0.0')] |
24 | 24 | class ExceptionalImplementable { |
25 | - public function __construct( |
|
26 | - protected string $app, |
|
27 | - protected ?string $class = null, |
|
28 | - ) { |
|
29 | - } |
|
25 | + public function __construct( |
|
26 | + protected string $app, |
|
27 | + protected ?string $class = null, |
|
28 | + ) { |
|
29 | + } |
|
30 | 30 | |
31 | - public function getApp(): string { |
|
32 | - return $this->app; |
|
33 | - } |
|
31 | + public function getApp(): string { |
|
32 | + return $this->app; |
|
33 | + } |
|
34 | 34 | |
35 | - public function getClass(): ?string { |
|
36 | - return $this->class; |
|
37 | - } |
|
35 | + public function getClass(): ?string { |
|
36 | + return $this->class; |
|
37 | + } |
|
38 | 38 | } |
@@ -18,17 +18,17 @@ |
||
18 | 18 | */ |
19 | 19 | #[Consumable(since: '32.0.0')] |
20 | 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 | - } |
|
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 | 30 | |
31 | - public function getSince(): string { |
|
32 | - return $this->since; |
|
33 | - } |
|
31 | + public function getSince(): string { |
|
32 | + return $this->since; |
|
33 | + } |
|
34 | 34 | } |
@@ -16,200 +16,200 @@ |
||
16 | 16 | use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent; |
17 | 17 | |
18 | 18 | class OcpSinceChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface { |
19 | - public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): void { |
|
20 | - $classLike = $event->getStmt(); |
|
21 | - $statementsSource = $event->getStatementsSource(); |
|
22 | - |
|
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 | - } |
|
35 | - |
|
36 | - foreach ($classLike->stmts as $stmt) { |
|
37 | - if ($stmt instanceof ClassConst) { |
|
38 | - self::checkStatementComment($stmt, $statementsSource, 'constant'); |
|
39 | - } |
|
40 | - |
|
41 | - if ($stmt instanceof ClassMethod) { |
|
42 | - self::checkStatementComment($stmt, $statementsSource, 'method'); |
|
43 | - } |
|
44 | - |
|
45 | - if ($stmt instanceof EnumCase) { |
|
46 | - if ($isTesting) { |
|
47 | - self::checkStatementAttributes($classLike, $statementsSource); |
|
48 | - } else { |
|
49 | - self::checkStatementComment($stmt, $statementsSource, 'enum'); |
|
50 | - } |
|
51 | - } |
|
52 | - } |
|
53 | - } |
|
54 | - |
|
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 | - |
|
104 | - private static function checkClassComment(ClassLike $stmt, FileSource $statementsSource): void { |
|
105 | - $docblock = $stmt->getDocComment(); |
|
106 | - |
|
107 | - if ($docblock === null) { |
|
108 | - IssueBuffer::maybeAdd( |
|
109 | - new InvalidDocblock( |
|
110 | - 'PHPDoc is required for classes/interfaces in OCP.', |
|
111 | - new CodeLocation($statementsSource, $stmt) |
|
112 | - ) |
|
113 | - ); |
|
114 | - return; |
|
115 | - } |
|
116 | - |
|
117 | - try { |
|
118 | - $parsedDocblock = DocComment::parsePreservingLength($docblock); |
|
119 | - } catch (DocblockParseException $e) { |
|
120 | - IssueBuffer::maybeAdd( |
|
121 | - new InvalidDocblock( |
|
122 | - $e->getMessage(), |
|
123 | - new CodeLocation($statementsSource, $stmt) |
|
124 | - ) |
|
125 | - ); |
|
126 | - return; |
|
127 | - } |
|
128 | - |
|
129 | - if (!isset($parsedDocblock->tags['since'])) { |
|
130 | - IssueBuffer::maybeAdd( |
|
131 | - new InvalidDocblock( |
|
132 | - '@since is required for classes/interfaces in OCP.', |
|
133 | - new CodeLocation($statementsSource, $stmt) |
|
134 | - ) |
|
135 | - ); |
|
136 | - } |
|
137 | - |
|
138 | - if (isset($parsedDocblock->tags['depreacted'])) { |
|
139 | - IssueBuffer::maybeAdd( |
|
140 | - new InvalidDocblock( |
|
141 | - 'Typo in @deprecated for classes/interfaces in OCP.', |
|
142 | - new CodeLocation($statementsSource, $stmt) |
|
143 | - ) |
|
144 | - ); |
|
145 | - } |
|
146 | - } |
|
147 | - |
|
148 | - private static function checkStatementComment(Stmt $stmt, FileSource $statementsSource, string $type): void { |
|
149 | - $docblock = $stmt->getDocComment(); |
|
150 | - |
|
151 | - if ($docblock === null) { |
|
152 | - IssueBuffer::maybeAdd( |
|
153 | - new InvalidDocblock( |
|
154 | - 'PHPDoc is required for ' . $type . 's in OCP.', |
|
155 | - new CodeLocation($statementsSource, $stmt) |
|
156 | - ), |
|
157 | - ); |
|
158 | - return; |
|
159 | - } |
|
160 | - |
|
161 | - try { |
|
162 | - $parsedDocblock = DocComment::parsePreservingLength($docblock); |
|
163 | - } catch (DocblockParseException $e) { |
|
164 | - IssueBuffer::maybeAdd( |
|
165 | - new InvalidDocblock( |
|
166 | - $e->getMessage(), |
|
167 | - new CodeLocation($statementsSource, $stmt) |
|
168 | - ) |
|
169 | - ); |
|
170 | - return; |
|
171 | - } |
|
172 | - |
|
173 | - if (!isset($parsedDocblock->tags['since'])) { |
|
174 | - IssueBuffer::maybeAdd( |
|
175 | - new InvalidDocblock( |
|
176 | - '@since is required for ' . $type . 's in OCP.', |
|
177 | - new CodeLocation($statementsSource, $stmt) |
|
178 | - ) |
|
179 | - ); |
|
180 | - } |
|
181 | - |
|
182 | - if (isset($parsedDocblock->tags['depreacted'])) { |
|
183 | - IssueBuffer::maybeAdd( |
|
184 | - new InvalidDocblock( |
|
185 | - 'Typo in @deprecated for ' . $type . ' in OCP.', |
|
186 | - new CodeLocation($statementsSource, $stmt) |
|
187 | - ) |
|
188 | - ); |
|
189 | - } |
|
190 | - } |
|
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 | - } |
|
19 | + public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): void { |
|
20 | + $classLike = $event->getStmt(); |
|
21 | + $statementsSource = $event->getStatementsSource(); |
|
22 | + |
|
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 | + } |
|
35 | + |
|
36 | + foreach ($classLike->stmts as $stmt) { |
|
37 | + if ($stmt instanceof ClassConst) { |
|
38 | + self::checkStatementComment($stmt, $statementsSource, 'constant'); |
|
39 | + } |
|
40 | + |
|
41 | + if ($stmt instanceof ClassMethod) { |
|
42 | + self::checkStatementComment($stmt, $statementsSource, 'method'); |
|
43 | + } |
|
44 | + |
|
45 | + if ($stmt instanceof EnumCase) { |
|
46 | + if ($isTesting) { |
|
47 | + self::checkStatementAttributes($classLike, $statementsSource); |
|
48 | + } else { |
|
49 | + self::checkStatementComment($stmt, $statementsSource, 'enum'); |
|
50 | + } |
|
51 | + } |
|
52 | + } |
|
53 | + } |
|
54 | + |
|
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 | + |
|
104 | + private static function checkClassComment(ClassLike $stmt, FileSource $statementsSource): void { |
|
105 | + $docblock = $stmt->getDocComment(); |
|
106 | + |
|
107 | + if ($docblock === null) { |
|
108 | + IssueBuffer::maybeAdd( |
|
109 | + new InvalidDocblock( |
|
110 | + 'PHPDoc is required for classes/interfaces in OCP.', |
|
111 | + new CodeLocation($statementsSource, $stmt) |
|
112 | + ) |
|
113 | + ); |
|
114 | + return; |
|
115 | + } |
|
116 | + |
|
117 | + try { |
|
118 | + $parsedDocblock = DocComment::parsePreservingLength($docblock); |
|
119 | + } catch (DocblockParseException $e) { |
|
120 | + IssueBuffer::maybeAdd( |
|
121 | + new InvalidDocblock( |
|
122 | + $e->getMessage(), |
|
123 | + new CodeLocation($statementsSource, $stmt) |
|
124 | + ) |
|
125 | + ); |
|
126 | + return; |
|
127 | + } |
|
128 | + |
|
129 | + if (!isset($parsedDocblock->tags['since'])) { |
|
130 | + IssueBuffer::maybeAdd( |
|
131 | + new InvalidDocblock( |
|
132 | + '@since is required for classes/interfaces in OCP.', |
|
133 | + new CodeLocation($statementsSource, $stmt) |
|
134 | + ) |
|
135 | + ); |
|
136 | + } |
|
137 | + |
|
138 | + if (isset($parsedDocblock->tags['depreacted'])) { |
|
139 | + IssueBuffer::maybeAdd( |
|
140 | + new InvalidDocblock( |
|
141 | + 'Typo in @deprecated for classes/interfaces in OCP.', |
|
142 | + new CodeLocation($statementsSource, $stmt) |
|
143 | + ) |
|
144 | + ); |
|
145 | + } |
|
146 | + } |
|
147 | + |
|
148 | + private static function checkStatementComment(Stmt $stmt, FileSource $statementsSource, string $type): void { |
|
149 | + $docblock = $stmt->getDocComment(); |
|
150 | + |
|
151 | + if ($docblock === null) { |
|
152 | + IssueBuffer::maybeAdd( |
|
153 | + new InvalidDocblock( |
|
154 | + 'PHPDoc is required for ' . $type . 's in OCP.', |
|
155 | + new CodeLocation($statementsSource, $stmt) |
|
156 | + ), |
|
157 | + ); |
|
158 | + return; |
|
159 | + } |
|
160 | + |
|
161 | + try { |
|
162 | + $parsedDocblock = DocComment::parsePreservingLength($docblock); |
|
163 | + } catch (DocblockParseException $e) { |
|
164 | + IssueBuffer::maybeAdd( |
|
165 | + new InvalidDocblock( |
|
166 | + $e->getMessage(), |
|
167 | + new CodeLocation($statementsSource, $stmt) |
|
168 | + ) |
|
169 | + ); |
|
170 | + return; |
|
171 | + } |
|
172 | + |
|
173 | + if (!isset($parsedDocblock->tags['since'])) { |
|
174 | + IssueBuffer::maybeAdd( |
|
175 | + new InvalidDocblock( |
|
176 | + '@since is required for ' . $type . 's in OCP.', |
|
177 | + new CodeLocation($statementsSource, $stmt) |
|
178 | + ) |
|
179 | + ); |
|
180 | + } |
|
181 | + |
|
182 | + if (isset($parsedDocblock->tags['depreacted'])) { |
|
183 | + IssueBuffer::maybeAdd( |
|
184 | + new InvalidDocblock( |
|
185 | + 'Typo in @deprecated for ' . $type . ' in OCP.', |
|
186 | + new CodeLocation($statementsSource, $stmt) |
|
187 | + ) |
|
188 | + ); |
|
189 | + } |
|
190 | + } |
|
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 | + } |
|
215 | 215 | } |