aboutsummaryrefslogtreecommitdiffstats
path: root/build/psalm
diff options
context:
space:
mode:
Diffstat (limited to 'build/psalm')
-rw-r--r--build/psalm/AttributeNamedParameters.php53
-rw-r--r--build/psalm/NcuExperimentalChecker.php131
-rw-r--r--build/psalm/OcpSinceChecker.php109
3 files changed, 286 insertions, 7 deletions
diff --git a/build/psalm/AttributeNamedParameters.php b/build/psalm/AttributeNamedParameters.php
new file mode 100644
index 00000000000..0b34cf3cf22
--- /dev/null
+++ b/build/psalm/AttributeNamedParameters.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+use PhpParser\Node\Attribute;
+use Psalm\CodeLocation;
+use Psalm\FileSource;
+use Psalm\Issue\InvalidDocblock;
+use Psalm\IssueBuffer;
+use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
+
+class AttributeNamedParameters implements Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface {
+ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): void {
+ $stmt = $event->getStmt();
+ $statementsSource = $event->getStatementsSource();
+
+ foreach ($stmt->attrGroups as $attrGroup) {
+ foreach ($attrGroup->attrs as $attr) {
+ self::checkAttribute($attr, $statementsSource);
+ }
+ }
+
+ foreach ($stmt->getMethods() as $method) {
+ foreach ($method->attrGroups as $attrGroup) {
+ foreach ($attrGroup->attrs as $attr) {
+ self::checkAttribute($attr, $statementsSource);
+ }
+ }
+ }
+ }
+
+ private static function checkAttribute(Attribute $stmt, FileSource $statementsSource): void {
+ if ($stmt->name->getLast() === 'Attribute') {
+ return;
+ }
+
+ foreach ($stmt->args as $arg) {
+ if ($arg->name === null) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'Attribute arguments must be named.',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+ }
+ }
+}
diff --git a/build/psalm/NcuExperimentalChecker.php b/build/psalm/NcuExperimentalChecker.php
new file mode 100644
index 00000000000..eaa70480ccc
--- /dev/null
+++ b/build/psalm/NcuExperimentalChecker.php
@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Stmt\ClassConst;
+use PhpParser\Node\Stmt\ClassLike;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\EnumCase;
+use Psalm\CodeLocation;
+use Psalm\DocComment;
+use Psalm\Exception\DocblockParseException;
+use Psalm\FileSource;
+use Psalm\Issue\InvalidDocblock;
+use Psalm\IssueBuffer;
+use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
+
+class NcuExperimentalChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface {
+ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): void {
+ $classLike = $event->getStmt();
+ $statementsSource = $event->getStatementsSource();
+
+ self::checkClassComment($classLike, $statementsSource);
+
+ foreach ($classLike->stmts as $stmt) {
+ if ($stmt instanceof ClassConst) {
+ self::checkStatementComment($stmt, $statementsSource, 'constant');
+ }
+
+ if ($stmt instanceof ClassMethod) {
+ self::checkStatementComment($stmt, $statementsSource, 'method');
+ }
+
+ if ($stmt instanceof EnumCase) {
+ self::checkStatementComment($stmt, $statementsSource, 'enum');
+ }
+ }
+ }
+
+ private static function checkClassComment(ClassLike $stmt, FileSource $statementsSource): void {
+ $docblock = $stmt->getDocComment();
+
+ if ($docblock === null) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'PHPDoc is required for classes/interfaces in NCU.',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ return;
+ }
+
+ try {
+ $parsedDocblock = DocComment::parsePreservingLength($docblock);
+ } catch (DocblockParseException $e) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ $e->getMessage(),
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ return;
+ }
+
+ if (!isset($parsedDocblock->tags['experimental'])) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ '@experimental is required for classes/interfaces in NCU.',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+
+ if (isset($parsedDocblock->tags['depreacted'])) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'Typo in @deprecated for classes/interfaces in NCU.',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+ }
+
+ private static function checkStatementComment(Stmt $stmt, FileSource $statementsSource, string $type): void {
+ $docblock = $stmt->getDocComment();
+
+ if ($docblock === null) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'PHPDoc is required for ' . $type . 's in NCU.',
+ new CodeLocation($statementsSource, $stmt)
+ ),
+ );
+ return;
+ }
+
+ try {
+ $parsedDocblock = DocComment::parsePreservingLength($docblock);
+ } catch (DocblockParseException $e) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ $e->getMessage(),
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ return;
+ }
+
+ if (!isset($parsedDocblock->tags['experimental'])) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ '@experimental is required for ' . $type . 's in NCU.',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+
+ if (isset($parsedDocblock->tags['depreacted'])) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'Typo in @deprecated for ' . $type . ' in NCU.',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+ }
+}
diff --git a/build/psalm/OcpSinceChecker.php b/build/psalm/OcpSinceChecker.php
index c030678aee1..38af92cf4ef 100644
--- a/build/psalm/OcpSinceChecker.php
+++ b/build/psalm/OcpSinceChecker.php
@@ -17,17 +17,88 @@ use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
class OcpSinceChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface {
public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): void {
- $stmt = $event->getStmt();
+ $classLike = $event->getStmt();
$statementsSource = $event->getStatementsSource();
- self::checkClassComment($stmt, $statementsSource);
+ if (!str_contains($statementsSource->getFilePath(), '/lib/public/')) {
+ return;
+ }
- foreach ($stmt->getMethods() as $method) {
- self::checkMethodOrConstantComment($method, $statementsSource, 'method');
+ $isTesting = str_contains($statementsSource->getFilePath(), '/lib/public/Notification/')
+ || str_contains($statementsSource->getFilePath(), '/lib/public/Config/')
+ || str_contains($statementsSource->getFilePath(), 'CalendarEventStatus');
+
+ if ($isTesting) {
+ self::checkStatementAttributes($classLike, $statementsSource);
+ } else {
+ self::checkClassComment($classLike, $statementsSource);
}
- foreach ($stmt->getConstants() as $constant) {
- self::checkMethodOrConstantComment($constant, $statementsSource, 'constant');
+ foreach ($classLike->stmts as $stmt) {
+ if ($stmt instanceof ClassConst) {
+ self::checkStatementComment($stmt, $statementsSource, 'constant');
+ }
+
+ if ($stmt instanceof ClassMethod) {
+ self::checkStatementComment($stmt, $statementsSource, 'method');
+ }
+
+ if ($stmt instanceof EnumCase) {
+ 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)
+ )
+ );
}
}
@@ -75,7 +146,7 @@ class OcpSinceChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitIn
}
}
- private static function checkMethodOrConstantComment(Stmt $stmt, FileSource $statementsSource, string $type): void {
+ private static function checkStatementComment(Stmt $stmt, FileSource $statementsSource, string $type): void {
$docblock = $stmt->getDocComment();
if ($docblock === null) {
@@ -118,4 +189,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)
+ )
+ );
+ }
+ }
+ }
+ }
+ }
}