Skip to content

Commit

Permalink
Check package tags
Browse files Browse the repository at this point in the history
Replaces the Package checks in moodle-local_moodlecheck
  • Loading branch information
andrewnicols committed Feb 19, 2024
1 parent 73f0a60 commit a0ffbd9
Show file tree
Hide file tree
Showing 32 changed files with 1,186 additions and 3 deletions.
233 changes: 233 additions & 0 deletions moodle/Sniffs/Commenting/PackageSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace MoodleHQ\MoodleCS\moodle\Sniffs\Commenting;

// phpcs:disable moodle.NamingConventions

use MoodleHQ\MoodleCS\moodle\Util\MoodleUtil;
use MoodleHQ\MoodleCS\moodle\Util\Docblocks;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHPCSUtils\Utils\ObjectDeclarations;

/**
* Checks that all test classes and global functions have appropriate @package tags.
*
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class PackageSniff implements Sniff {

/**
* Register for open tag (only process once per file).
*/
public function register() {
return [
T_OPEN_TAG,
];
}

/**
* Processes php files and perform various checks with file.
*
* @param File $phpcsFile The file being scanned.
* @param int $stackPtr The position in the stack.
*/
public function process(File $phpcsFile, $stackPtr) {
$tokens = $phpcsFile->getTokens();

$docblock = Docblocks::getDocBlock($phpcsFile, $stackPtr);
if ($docblock) {
$filePackageFound = $this->checkDocblock(
$phpcsFile,
$stackPtr,
$docblock
);
if ($filePackageFound) {
return;
}
}

$find = [
T_CLASS,
T_FUNCTION,
T_TRAIT,
T_INTERFACE,
];
$typePtr = $stackPtr + 1;
while ($typePtr = $phpcsFile->findNext($find, $typePtr + 1)) {
$token = $tokens[$typePtr];
if ($token['code'] === T_FUNCTION && !empty($token['conditions'])) {
// Skip methods of classes, traits and interfaces.
continue;
}

$docblock = Docblocks::getDocBlock($phpcsFile, $typePtr);

if ($docblock === null) {
$objectName = $this->getObjectName($phpcsFile, $typePtr);
$objectType = $this->getObjectType($phpcsFile, $typePtr);
$phpcsFile->addError('Missing doc comment for %s %s', $typePtr, 'Missing', [$objectType, $objectName]);

continue;
}

$this->checkDocblock($phpcsFile, $typePtr, $docblock);
}

}

/**
* Get the human-readable object type.
*
* @param File $phpcsFile
* @param int $stackPtr
* @return string
*/
protected function getObjectType(
File $phpcsFile,
int $stackPtr
): string {
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_OPEN_TAG) {
return 'file';
}
return $tokens[$stackPtr]['content'];
}

/**
* Get the human readable object name.
*
* @param File $phpcsFile
* @param int $stackPtr
* @return string
*/
protected function getObjectName(
File $phpcsFile,
int $stackPtr
): string {
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['code'] === T_OPEN_TAG) {
return basename($phpcsFile->getFilename());
}

return ObjectDeclarations::getName($phpcsFile, $stackPtr);
}

/**
* Check the docblock for a @package tag.
*
* @param File $phpcsFile
* @param int $stackPtr
* @param array $docblock
* @return bool Whether any package tag was found, whether or not it was correct
*/
protected function checkDocblock(
File $phpcsFile,
int $stackPtr,
array $docblock
): bool {
$tokens = $phpcsFile->getTokens();
$objectName = $this->getObjectName($phpcsFile, $stackPtr);
$objectType = $this->getObjectType($phpcsFile, $stackPtr);
$expectedPackage = MoodleUtil::getMoodleComponent($phpcsFile, true);

$packageTokens = Docblocks::getMatchingDocTags($phpcsFile, $stackPtr, '@package');
if (empty($packageTokens)) {
$fix = $phpcsFile->addFixableError(
'DocBlock missing a @package tag for %s %s. Expected @package %s',
$stackPtr,
'Missing',
[$objectType, $objectName, $expectedPackage]
);

if ($fix) {
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->addContentBefore($docblock['comment_closer'], '* @package ' . $expectedPackage . PHP_EOL . ' ');
$phpcsFile->fixer->endChangeset();
}

return false;
}

if (count($packageTokens) > 1) {
$fix = $phpcsFile->addFixableError(
'More than one @package tag found in %s %s.',
$stackPtr,
'Multiple',
[$objectType, $objectName]
);

if ($fix) {
$phpcsFile->fixer->beginChangeset();
$validTokenFound = false;

foreach ($packageTokens as $i => $packageToken) {
$packageValuePtr = $phpcsFile->findNext(
T_DOC_COMMENT_STRING,
$packageToken,
$docblock['comment_closer']
);
$packageValue = $tokens[$packageValuePtr]['content'];
if (!$validTokenFound && $packageValue === $expectedPackage) {
$validTokenFound = true;
continue;
}
$lineNo = $tokens[$packageToken]['line'];
foreach (array_keys(MoodleUtil::getTokensOnLine($phpcsFile, $lineNo)) as $lineToken) {
$phpcsFile->fixer->replaceToken($lineToken, '');
}
}
if (!$validTokenFound) {
$phpcsFile->fixer->addContentBefore($packageTokens[0], ' * @package ' . $expectedPackage . PHP_EOL);
}
$phpcsFile->fixer->endChangeset();
}
return true;
}

$packageToken = reset($packageTokens);

// Check the value of the package tag.
$packageValuePtr = $phpcsFile->findNext(
T_DOC_COMMENT_STRING,
$packageToken,
$docblock['comment_closer']
);
$packageValue = $tokens[$packageValuePtr]['content'];

// Compare to expected value.
if ($packageValue === $expectedPackage) {
return true;
}

$fix = $phpcsFile->addFixableError(
'Incorrect @package tag for %s %s. Expected %s, found %s.',
$packageToken,
'Incorrect',
[$objectType, $objectName, $expectedPackage, $packageValue]
);

if ($fix) {
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->replaceToken($packageValuePtr, $expectedPackage);
$phpcsFile->fixer->endChangeset();
}

return true;
}
}
95 changes: 95 additions & 0 deletions moodle/Tests/Sniffs/Commenting/PackageSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.

namespace MoodleHQ\MoodleCS\moodle\Tests\Sniffs\Commenting;

use MoodleHQ\MoodleCS\moodle\Tests\MoodleCSBaseTestCase;

// phpcs:disable moodle.NamingConventions

/**
* Test the TestCaseNamesSniff sniff.
*
* @category test
* @copyright 2024 onwards Andrew Lyons <andrew@nicols.co.uk>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @covers \MoodleHQ\MoodleCS\moodle\Sniffs\Commenting\PackageSniff
*/
class PackageSniffTest extends MoodleCSBaseTestCase
{

/**
* @dataProvider package_correctness_provider
*/
public function test_package_correctness(
string $fixture,
array $errors,
array $warnings
): void {
$this->set_standard('moodle');
$this->set_sniff('moodle.Commenting.Package');
$this->set_fixture(sprintf("%s/fixtures/%s.php", __DIR__, $fixture));
$this->set_warnings($warnings);
$this->set_errors($errors);
$this->set_component_mapping([
'local_codechecker' => dirname(__DIR__),
]);

$this->verify_cs_results();
}

public static function package_correctness_provider(): array {
return [
'Standard fixes' => [
'fixture' => 'package_tags',
'errors' => [
18 => 'DocBlock missing a @package tag for function package_missing. Expected @package local_codechecker',
31 => 'DocBlock missing a @package tag for class package_absent. Expected @package local_codechecker',
34 => 'Missing doc comment for function missing_docblock_in_function',
38 => 'Missing doc comment for class missing_docblock_in_class',
42 => 'Incorrect @package tag for function package_wrong_in_function. Expected local_codechecker, found wrong_package.',
48 => 'Incorrect @package tag for class package_wrong_in_class. Expected local_codechecker, found wrong_package.',
57 => 'More than one @package tag found in function package_multiple_in_function',
64 => 'More than one @package tag found in class package_multiple_in_class',
71 => 'More than one @package tag found in function package_multiple_in_function_all_wrong',
78 => 'More than one @package tag found in class package_multiple_in_class_all_wrong',
85 => 'More than one @package tag found in interface package_multiple_in_interface_all_wrong',
92 => 'More than one @package tag found in trait package_multiple_in_trait_all_wrong',
95 => 'Missing doc comment for interface missing_docblock_interface',
101 => 'DocBlock missing a @package tag for interface missing_package_interface. Expected @package local_codechecker',
106 => 'Incorrect @package tag for interface incorrect_package_interface. Expected local_codechecker, found local_codecheckers.',
118 => 'Missing doc comment for trait missing_docblock_trait',
124 => 'DocBlock missing a @package tag for trait missing_package_trait. Expected @package local_codechecker',
129 => 'Incorrect @package tag for trait incorrect_package_trait. Expected local_codechecker, found local_codecheckers.',
],
'warnings' => [],
],
'File level tag (wrong)' => [
'fixture' => 'package_tags_file_wrong',
'errors' => [
20 => 'Incorrect @package tag for file package_tags_file_wrong.php. Expected local_codechecker, found core.',
],
'warnings' => [],
],
'File level tag (right)' => [
'fixture' => 'package_tags_file_right',
'errors' => [],
'warnings' => [],
],
];
}
}
Loading

0 comments on commit a0ffbd9

Please sign in to comment.