diff options
Diffstat (limited to 'build/psalm')
-rw-r--r-- | build/psalm/AppFrameworkTainter.php | 75 | ||||
-rw-r--r-- | build/psalm/AttributeNamedParameters.php | 53 | ||||
-rw-r--r-- | build/psalm/NcuExperimentalChecker.php | 131 | ||||
-rw-r--r-- | build/psalm/OcpSinceChecker.php | 216 |
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) + ) + ); + } + } + } + } + } +} |