aboutsummaryrefslogtreecommitdiffstats
path: root/build/psalm
diff options
context:
space:
mode:
Diffstat (limited to 'build/psalm')
-rw-r--r--build/psalm/AppFrameworkTainter.php23
-rw-r--r--build/psalm/AttributeNamedParameters.php53
-rw-r--r--build/psalm/NcuExperimentalChecker.php131
-rw-r--r--build/psalm/OcpSinceChecker.php129
4 files changed, 290 insertions, 46 deletions
diff --git a/build/psalm/AppFrameworkTainter.php b/build/psalm/AppFrameworkTainter.php
index 9a68885b175..448922d25a5 100644
--- a/build/psalm/AppFrameworkTainter.php
+++ b/build/psalm/AppFrameworkTainter.php
@@ -1,28 +1,9 @@
<?php
/**
- * Copyright (c) 2020 Lukas Reschke <lukas@statuscode.ch>
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: MIT
*/
-
use Psalm\CodeLocation;
use Psalm\Plugin\EventHandler\AfterFunctionLikeAnalysisInterface;
use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent;
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 92437a8f3ab..38af92cf4ef 100644
--- a/build/psalm/OcpSinceChecker.php
+++ b/build/psalm/OcpSinceChecker.php
@@ -1,26 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright 2023 Daniel Kesselberg <mail@danielkesselberg.de>
- *
- * @author 2023 Daniel Kesselberg <mail@danielkesselberg.de>
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\ClassLike;
use Psalm\CodeLocation;
@@ -33,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)
+ )
+ );
}
}
@@ -91,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) {
@@ -134,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)
+ )
+ );
+ }
+ }
+ }
+ }
+ }
}