aboutsummaryrefslogtreecommitdiffstats
path: root/build/psalm
diff options
context:
space:
mode:
Diffstat (limited to 'build/psalm')
-rw-r--r--build/psalm/AppFrameworkTainter.php75
-rw-r--r--build/psalm/AttributeNamedParameters.php53
-rw-r--r--build/psalm/NcuExperimentalChecker.php131
-rw-r--r--build/psalm/OcpSinceChecker.php216
4 files changed, 427 insertions, 48 deletions
diff --git a/build/psalm/AppFrameworkTainter.php b/build/psalm/AppFrameworkTainter.php
index c12dcfc6ce2..448922d25a5 100644
--- a/build/psalm/AppFrameworkTainter.php
+++ b/build/psalm/AppFrameworkTainter.php
@@ -1,60 +1,39 @@
<?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\Hook\AfterFunctionLikeAnalysisInterface;
+use Psalm\Plugin\EventHandler\AfterFunctionLikeAnalysisInterface;
+use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent;
use Psalm\Type\TaintKindGroup;
class AppFrameworkTainter implements AfterFunctionLikeAnalysisInterface {
- public static function afterStatementAnalysis(
- PhpParser\Node\FunctionLike $stmt,
- Psalm\Storage\FunctionLikeStorage $classlike_storage,
- Psalm\StatementsSource $statements_source,
- Psalm\Codebase $codebase,
- array &$file_replacements = []
- ): ?bool {
- if ($statements_source->getFQCLN() !== null) {
- if ($codebase->classExtendsOrImplements($statements_source->getFQCLN(), \OCP\AppFramework\Controller::class)) {
- if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
- if ($stmt->isPublic() && !$stmt->isMagic()) {
- foreach ($stmt->params as $i => $param) {
- $expr_type = new Psalm\Type\Union([new Psalm\Type\Atomic\TString()]);
- $expr_identifier = (strtolower($statements_source->getFQCLN()) . '::' . strtolower($classlike_storage->cased_name) . '#' . ($i + 1));
-
- if ($expr_type) {
- $codebase->addTaintSource(
- $expr_type,
- $expr_identifier,
- TaintKindGroup::ALL_INPUT,
- new CodeLocation($statements_source, $param)
- );
- }
- }
- }
- }
- }
+ public static function afterStatementAnalysis(AfterFunctionLikeAnalysisEvent $event): ?bool {
+ if ($event->getStatementsSource()->getFQCLN() === null) {
+ return null;
+ }
+ if (!$event->getCodebase()->classExtendsOrImplements($event->getStatementsSource()->getFQCLN(), \OCP\AppFramework\Controller::class)) {
+ return null;
+ }
+ if (!($event->getStmt() instanceof PhpParser\Node\Stmt\ClassMethod)) {
+ return null;
}
+ if (!$event->getStmt()->isPublic() || $event->getStmt()->isMagic()) {
+ return null;
+ }
+ foreach ($event->getStmt()->params as $i => $param) {
+ $expr_type = new Psalm\Type\Union([new Psalm\Type\Atomic\TString()]);
+ $expr_identifier = (strtolower($event->getStatementsSource()->getFQCLN()) . '::' . strtolower($event->getFunctionlikeStorage()->cased_name) . '#' . ($i + 1));
+ $event->getCodebase()->addTaintSource(
+ $expr_type,
+ $expr_identifier,
+ TaintKindGroup::ALL_INPUT,
+ new CodeLocation($event->getStatementsSource(), $param)
+ );
+ }
+
return null;
}
}
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
new file mode 100644
index 00000000000..38af92cf4ef
--- /dev/null
+++ b/build/psalm/OcpSinceChecker.php
@@ -0,0 +1,216 @@
+<?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\ClassLike;
+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 OcpSinceChecker implements Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface {
+ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): void {
+ $classLike = $event->getStmt();
+ $statementsSource = $event->getStatementsSource();
+
+ if (!str_contains($statementsSource->getFilePath(), '/lib/public/')) {
+ return;
+ }
+
+ $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 ($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)
+ )
+ );
+ }
+ }
+
+ 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 OCP.',
+ 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['since'])) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ '@since is required for classes/interfaces in OCP.',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+
+ if (isset($parsedDocblock->tags['depreacted'])) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'Typo in @deprecated for classes/interfaces in OCP.',
+ 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 OCP.',
+ 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['since'])) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ '@since is required for ' . $type . 's in OCP.',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+
+ if (isset($parsedDocblock->tags['depreacted'])) {
+ IssueBuffer::maybeAdd(
+ new InvalidDocblock(
+ 'Typo in @deprecated for ' . $type . ' in OCP.',
+ new CodeLocation($statementsSource, $stmt)
+ )
+ );
+ }
+ }
+
+ 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)
+ )
+ );
+ }
+ }
+ }
+ }
+ }
+}