Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check package tags #110

Merged
merged 1 commit into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt
## [Unreleased]
### Changed
- Update composer dependencies to current versions, notably `PHP_CodeSniffer` (3.9.0) and `PHPCompatibility` (e5cd2e24).
- Add new moodle.Commenting.Package sniffs to replace those present in moodle-local_moodlecheck.

## [v3.3.15] - 2024-02-15
### Added
Expand Down
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 <[email protected]>
* @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 <[email protected]>
* @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