diff --git a/build/psalm/OcpSinceChecker.php b/build/psalm/OcpSinceChecker.php index 959e70e0c4ce6..c533e94455084 100644 --- a/build/psalm/OcpSinceChecker.php +++ b/build/psalm/OcpSinceChecker.php @@ -20,7 +20,18 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): voi $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 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): voi } 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 @@ private static function checkStatementComment(Stmt $stmt, FileSource $statements ); } } + + 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 aff727bedb406..8d49119ad76b8 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -58,6 +58,14 @@ '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', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index a73ce3c13d668..1534985ff8a4f 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.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', diff --git a/lib/public/AppFramework/Attribute/ASince.php b/lib/public/AppFramework/Attribute/ASince.php new file mode 100644 index 0000000000000..1e0c45348cfc2 --- /dev/null +++ b/lib/public/AppFramework/Attribute/ASince.php @@ -0,0 +1,34 @@ +since; + } +} diff --git a/lib/public/AppFramework/Attribute/Catchable.php b/lib/public/AppFramework/Attribute/Catchable.php new file mode 100644 index 0000000000000..d45401550f6f7 --- /dev/null +++ b/lib/public/AppFramework/Attribute/Catchable.php @@ -0,0 +1,23 @@ +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 0000000000000..40ce0e0cf0638 --- /dev/null +++ b/lib/public/AppFramework/Attribute/Implementable.php @@ -0,0 +1,27 @@ +