diff --git a/moodle/Sniffs/Commenting/PackageSniff.php b/moodle/Sniffs/Commenting/PackageSniff.php new file mode 100644 index 0000000..85e0a96 --- /dev/null +++ b/moodle/Sniffs/Commenting/PackageSniff.php @@ -0,0 +1,233 @@ +. + +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 + * @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; + } +} diff --git a/moodle/Tests/Sniffs/Commenting/PackageSniffTest.php b/moodle/Tests/Sniffs/Commenting/PackageSniffTest.php new file mode 100644 index 0000000..c4ee0ff --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/PackageSniffTest.php @@ -0,0 +1,95 @@ +. + +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 + * @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' => [], + ], + ]; + } +} diff --git a/moodle/Tests/Sniffs/Commenting/fixtures/package_tags.php b/moodle/Tests/Sniffs/Commenting/fixtures/package_tags.php new file mode 100644 index 0000000..19dd29e --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/fixtures/package_tags.php @@ -0,0 +1,168 @@ +. + +/** + * This file contains the core_userfeedback class + * + * @package local_codechecker + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * This Class contains helper functions for user feedback functionality. + */ +class example { +} diff --git a/moodle/Tests/Sniffs/Commenting/fixtures/package_tags_file_wrong.php b/moodle/Tests/Sniffs/Commenting/fixtures/package_tags_file_wrong.php new file mode 100644 index 0000000..1b59dd9 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/fixtures/package_tags_file_wrong.php @@ -0,0 +1,29 @@ +. + +/** + * This file contains the core_userfeedback class + * + * @package core + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * This Class contains helper functions for user feedback functionality. + */ +class example { +} diff --git a/moodle/Tests/Sniffs/Commenting/fixtures/package_tags_file_wrong.php.fixed b/moodle/Tests/Sniffs/Commenting/fixtures/package_tags_file_wrong.php.fixed new file mode 100644 index 0000000..d601909 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/fixtures/package_tags_file_wrong.php.fixed @@ -0,0 +1,29 @@ +. + +/** + * This file contains the core_userfeedback class + * + * @package local_codechecker + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * This Class contains helper functions for user feedback functionality. + */ +class example { +} diff --git a/moodle/Tests/Sniffs/PHPUnit/TestcaseAbstractSniffTest.php b/moodle/Tests/Sniffs/PHPUnit/TestCasesAbstractSniffTest.php similarity index 93% rename from moodle/Tests/Sniffs/PHPUnit/TestcaseAbstractSniffTest.php rename to moodle/Tests/Sniffs/PHPUnit/TestCasesAbstractSniffTest.php index f9475a0..bbdf095 100644 --- a/moodle/Tests/Sniffs/PHPUnit/TestcaseAbstractSniffTest.php +++ b/moodle/Tests/Sniffs/PHPUnit/TestCasesAbstractSniffTest.php @@ -26,9 +26,9 @@ * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * - * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\PHPUnit\TestcaseAbstractSniff + * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\PHPUnit\TestCasesAbstractSniff */ -class TestcaseAbstractSniffTest extends MoodleCSBaseTestCase { +class TestCasesAbstractSniffTest extends MoodleCSBaseTestCase { /** * Data provider for self::provider_phpunit_data_returntypes diff --git a/moodle/Tests/Util/DocblocksTest.php b/moodle/Tests/Util/DocblocksTest.php new file mode 100644 index 0000000..bc0e444 --- /dev/null +++ b/moodle/Tests/Util/DocblocksTest.php @@ -0,0 +1,108 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Tests\Util; + +use MoodleHQ\MoodleCS\moodle\Tests\MoodleCSBaseTestCase; +use MoodleHQ\MoodleCS\moodle\Util\Docblocks; +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Ruleset; + +// phpcs:disable moodle.NamingConventions + +/** + * Test the Docblocks specific moodle utilities class + * + * @package local_codechecker + * @category test + * @copyright 2021 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \MoodleHQ\MoodleCS\moodle\Util\Docblocks + */ +class DocblocksTest extends MoodleCSBaseTestCase { + public function testGetDocBlock(): void { + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $phpcsFile = new \PHP_CodeSniffer\Files\LocalFile( + __DIR__ . '/fixtures/docblocks/none.php', + $phpcsRuleset, + $phpcsConfig + ); + + $phpcsFile->process(); + $filePointer = $phpcsFile->findNext(T_OPEN_TAG, 0); + + $docBlock = Docblocks::getDocBlock($phpcsFile, $filePointer); + $this->assertNull($docBlock); + } + + public function testGetDocBlockTags(): void { + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $phpcsFile = new \PHP_CodeSniffer\Files\LocalFile( + __DIR__ . '/fixtures/docblocks/class_docblock.php', + $phpcsRuleset, + $phpcsConfig + ); + + $phpcsFile->process(); + $filePointer = $phpcsFile->findNext(T_OPEN_TAG, 0); + $classPointer = $phpcsFile->findNext(T_CLASS, 0); + + $fileDocBlock = Docblocks::getDocBlock($phpcsFile, $filePointer); + $this->assertNotNull($fileDocBlock); + $this->assertCount(1, Docblocks::getMatchingDocTags($phpcsFile, $filePointer, '@copyright')); + $this->assertCount(0, Docblocks::getMatchingDocTags($phpcsFile, $filePointer, '@property')); + + $classDocBlock = Docblocks::getDocBlock($phpcsFile, $classPointer); + $this->assertNotNull($classDocBlock); + $this->assertNotEquals($fileDocBlock, $classDocBlock); + $this->assertCount(1, Docblocks::getMatchingDocTags($phpcsFile, $classPointer, '@copyright')); + $this->assertCount(2, Docblocks::getMatchingDocTags($phpcsFile, $classPointer, '@property')); + + $methodPointer = $phpcsFile->findNext(T_FUNCTION, $classPointer); + $this->assertNull(Docblocks::getDocBlock($phpcsFile, $methodPointer)); + $this->assertCount(0, Docblocks::getMatchingDocTags($phpcsFile, $methodPointer, '@property')); + } + + public function testGetDocBlockClassOnly(): void { + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $phpcsFile = new \PHP_CodeSniffer\Files\LocalFile( + __DIR__ . '/fixtures/docblocks/class_docblock_only.php', + $phpcsRuleset, + $phpcsConfig + ); + + $phpcsFile->process(); + $filePointer = $phpcsFile->findNext(T_OPEN_TAG, 0); + $classPointer = $phpcsFile->findNext(T_CLASS, 0); + + $fileDocBlock = Docblocks::getDocBlock($phpcsFile, $filePointer); + $this->assertNull($fileDocBlock); + + $classDocBlock = Docblocks::getDocBlock($phpcsFile, $classPointer); + $this->assertNotNull($classDocBlock); + $this->assertNotEquals($fileDocBlock, $classDocBlock); + $this->assertCount(1, Docblocks::getMatchingDocTags($phpcsFile, $classPointer, '@copyright')); + $this->assertCount(2, Docblocks::getMatchingDocTags($phpcsFile, $classPointer, '@property')); + + $methodPointer = $phpcsFile->findNext(T_FUNCTION, $classPointer); + $this->assertNull(Docblocks::getDocBlock($phpcsFile, $methodPointer)); + $this->assertCount(0, Docblocks::getMatchingDocTags($phpcsFile, $methodPointer, '@property')); + } +} diff --git a/moodle/Tests/MoodleUtilTest.php b/moodle/Tests/Util/MoodleUtilTest.php similarity index 96% rename from moodle/Tests/MoodleUtilTest.php rename to moodle/Tests/Util/MoodleUtilTest.php index 515db7f..3d1b65f 100644 --- a/moodle/Tests/MoodleUtilTest.php +++ b/moodle/Tests/Util/MoodleUtilTest.php @@ -14,8 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace MoodleHQ\MoodleCS\moodle\Tests; +namespace MoodleHQ\MoodleCS\moodle\Tests\Util; +use MoodleHQ\MoodleCS\moodle\Tests\MoodleCSBaseTestCase; use MoodleHQ\MoodleCS\moodle\Util\MoodleUtil; use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Exceptions\DeepExitException; @@ -746,4 +747,31 @@ public function testFindClassMethodPointer( $this->assertNull($pointer); } } + + public function testGetTokensOnLine(): void + { + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $phpcsFile = new \PHP_CodeSniffer\Files\LocalFile( + __DIR__ . '/fixtures/moodleutil/test_with_methods_to_find.php', + $phpcsRuleset, + $phpcsConfig + ); + + $phpcsFile->process(); + $allTokens = $phpcsFile->getTokens(); + + $expectedTokens = []; + foreach ($allTokens as $tokenPtr => $token) { + if ($token['line'] === 8) { + $expectedTokens[$tokenPtr] = $token; + } + } + + // This line is: + // protected function protected_method(): array { + $tokens = MoodleUtil::getTokensOnLine($phpcsFile, 8); + $this->assertCount(count($expectedTokens), $tokens); + $this->assertEquals($expectedTokens, $tokens); + } } diff --git a/moodle/Tests/Util/fixtures/docblocks/class_docblock.php b/moodle/Tests/Util/fixtures/docblocks/class_docblock.php new file mode 100644 index 0000000..29dfefa --- /dev/null +++ b/moodle/Tests/Util/fixtures/docblocks/class_docblock.php @@ -0,0 +1,41 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Tests; + +use SomeDependency; + +/** + * Example File level docblock. + * + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Example class. + * + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @property string $property + * @property string $name + */ +#[\Attribute] +class example extends SomeDependency { + public static function exampleMethod() { + return 'example'; + } +} diff --git a/moodle/Tests/Util/fixtures/docblocks/class_docblock_only.php b/moodle/Tests/Util/fixtures/docblocks/class_docblock_only.php new file mode 100644 index 0000000..4b9eec3 --- /dev/null +++ b/moodle/Tests/Util/fixtures/docblocks/class_docblock_only.php @@ -0,0 +1,34 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Tests; + +use SomeDependency; + +/** + * Example class. + * + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @property string $property + * @property string $name + */ +#[\Attribute] +class example extends SomeDependency { + public static function exampleMethod() { + return 'example'; + } +} diff --git a/moodle/Tests/Util/fixtures/docblocks/file_and_class_docblock.php b/moodle/Tests/Util/fixtures/docblocks/file_and_class_docblock.php new file mode 100644 index 0000000..429e0c3 --- /dev/null +++ b/moodle/Tests/Util/fixtures/docblocks/file_and_class_docblock.php @@ -0,0 +1,26 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Tests; + +/** + * Example class. + * + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class example { +} diff --git a/moodle/Tests/Util/fixtures/docblocks/none.php b/moodle/Tests/Util/fixtures/docblocks/none.php new file mode 100644 index 0000000..3b6c583 --- /dev/null +++ b/moodle/Tests/Util/fixtures/docblocks/none.php @@ -0,0 +1,20 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Tests; + +class example { +} diff --git a/moodle/Tests/fixtures/moodleutil/bad/config-dist.php b/moodle/Tests/Util/fixtures/moodleutil/bad/config-dist.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/bad/config-dist.php rename to moodle/Tests/Util/fixtures/moodleutil/bad/config-dist.php diff --git a/moodle/Tests/fixtures/moodleutil/bad/lib/lib.php b/moodle/Tests/Util/fixtures/moodleutil/bad/lib/lib.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/bad/lib/lib.php rename to moodle/Tests/Util/fixtures/moodleutil/bad/lib/lib.php diff --git a/moodle/Tests/fixtures/moodleutil/bad/version.php b/moodle/Tests/Util/fixtures/moodleutil/bad/version.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/bad/version.php rename to moodle/Tests/Util/fixtures/moodleutil/bad/version.php diff --git a/moodle/Tests/fixtures/moodleutil/complete/config-dist.php b/moodle/Tests/Util/fixtures/moodleutil/complete/config-dist.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/complete/config-dist.php rename to moodle/Tests/Util/fixtures/moodleutil/complete/config-dist.php diff --git a/moodle/Tests/fixtures/moodleutil/complete/lib/classes/component.php b/moodle/Tests/Util/fixtures/moodleutil/complete/lib/classes/component.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/complete/lib/classes/component.php rename to moodle/Tests/Util/fixtures/moodleutil/complete/lib/classes/component.php diff --git a/moodle/Tests/fixtures/moodleutil/complete/lib/components.json b/moodle/Tests/Util/fixtures/moodleutil/complete/lib/components.json similarity index 100% rename from moodle/Tests/fixtures/moodleutil/complete/lib/components.json rename to moodle/Tests/Util/fixtures/moodleutil/complete/lib/components.json diff --git a/moodle/Tests/fixtures/moodleutil/complete/lib/lib.php b/moodle/Tests/Util/fixtures/moodleutil/complete/lib/lib.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/complete/lib/lib.php rename to moodle/Tests/Util/fixtures/moodleutil/complete/lib/lib.php diff --git a/moodle/Tests/fixtures/moodleutil/complete/local/invented/lib.php b/moodle/Tests/Util/fixtures/moodleutil/complete/local/invented/lib.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/complete/local/invented/lib.php rename to moodle/Tests/Util/fixtures/moodleutil/complete/local/invented/lib.php diff --git a/moodle/Tests/fixtures/moodleutil/complete/version.php b/moodle/Tests/Util/fixtures/moodleutil/complete/version.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/complete/version.php rename to moodle/Tests/Util/fixtures/moodleutil/complete/version.php diff --git a/moodle/Tests/fixtures/moodleutil/good/config-dist.php b/moodle/Tests/Util/fixtures/moodleutil/good/config-dist.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/good/config-dist.php rename to moodle/Tests/Util/fixtures/moodleutil/good/config-dist.php diff --git a/moodle/Tests/fixtures/moodleutil/good/lib/lib.php b/moodle/Tests/Util/fixtures/moodleutil/good/lib/lib.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/good/lib/lib.php rename to moodle/Tests/Util/fixtures/moodleutil/good/lib/lib.php diff --git a/moodle/Tests/fixtures/moodleutil/good/version.php b/moodle/Tests/Util/fixtures/moodleutil/good/version.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/good/version.php rename to moodle/Tests/Util/fixtures/moodleutil/good/version.php diff --git a/moodle/Tests/fixtures/moodleutil/istestcaseclass/is_testcase.php b/moodle/Tests/Util/fixtures/moodleutil/istestcaseclass/is_testcase.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/istestcaseclass/is_testcase.php rename to moodle/Tests/Util/fixtures/moodleutil/istestcaseclass/is_testcase.php diff --git a/moodle/Tests/fixtures/moodleutil/istestcaseclass/multiple_classes_test.php b/moodle/Tests/Util/fixtures/moodleutil/istestcaseclass/multiple_classes_test.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/istestcaseclass/multiple_classes_test.php rename to moodle/Tests/Util/fixtures/moodleutil/istestcaseclass/multiple_classes_test.php diff --git a/moodle/Tests/fixtures/moodleutil/istestcaseclass/not_testcase.php b/moodle/Tests/Util/fixtures/moodleutil/istestcaseclass/not_testcase.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/istestcaseclass/not_testcase.php rename to moodle/Tests/Util/fixtures/moodleutil/istestcaseclass/not_testcase.php diff --git a/moodle/Tests/fixtures/moodleutil/test_with_methods_to_find.php b/moodle/Tests/Util/fixtures/moodleutil/test_with_methods_to_find.php similarity index 100% rename from moodle/Tests/fixtures/moodleutil/test_with_methods_to_find.php rename to moodle/Tests/Util/fixtures/moodleutil/test_with_methods_to_find.php diff --git a/moodle/Util/Docblocks.php b/moodle/Util/Docblocks.php new file mode 100644 index 0000000..86b91f0 --- /dev/null +++ b/moodle/Util/Docblocks.php @@ -0,0 +1,159 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Util; + +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Exceptions\DeepExitException; +use PHP_CodeSniffer\Files\DummyFile; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Ruleset; + +// phpcs:disable moodle.NamingConventions + +/** + * Utilities related to PHP DocBlocks. + * + * @package local_codechecker + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class Docblocks { + /** + * Get the docblock for a file, class, interface, trait, or method. + * + * @param File $phpcsFile + * @param int $stackPtr + * @return null|array + */ + public static function getDocBlock( + File $phpcsFile, + int $stackPtr + ): ?array { + $tokens = $phpcsFile->getTokens(); + $find = [ + T_ABSTRACT => T_ABSTRACT, + T_FINAL => T_FINAL, + T_READONLY => T_READONLY, + T_WHITESPACE => T_WHITESPACE, + ]; + + if ($tokens[$stackPtr]['code'] === T_OPEN_TAG) { + $ignore = [ + T_WHITESPACE, + T_COMMENT, + ]; + + $stopAtTypes = [ + T_CLASS, + T_INTERFACE, + T_TRAIT, + T_ENUM, + T_FUNCTION, + T_CLOSURE, + T_PUBLIC, + T_PRIVATE, + T_PROTECTED, + T_FINAL, + T_STATIC, + T_ABSTRACT, + T_READONLY, + T_CONST, + T_PROPERTY, + T_INCLUDE, + T_INCLUDE_ONCE, + T_REQUIRE, + T_REQUIRE_ONCE, + ]; + + while ($stackPtr = $phpcsFile->findNext($ignore, ($stackPtr + 1), null, true)) { + if ($tokens[$stackPtr]['code'] === T_NAMESPACE || $tokens[$stackPtr]['code'] === T_USE) { + $stackPtr = $phpcsFile->findNext(T_SEMICOLON, $stackPtr + 1); + continue; + } + + if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) { + $commentEnd = $tokens[$stackPtr]['comment_closer']; + $nextToken = $commentEnd; + while ($nextToken = $phpcsFile->findNext(T_WHITESPACE, $nextToken + 1, null, true)) { + if ($nextToken && $tokens[$nextToken]['code'] === T_ATTRIBUTE) { + $nextToken = $tokens[$nextToken]['attribute_closer'] + 1; + continue; + } + if (in_array($tokens[$nextToken]['code'], $stopAtTypes)) { + return null; + } + break; + } + return $tokens[$stackPtr]; + } + } + + return null; + } + + + $previousContent = null; + for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) { + if (isset($find[$tokens[$commentEnd]['code']]) === true) { + continue; + } + + if ($previousContent === null) { + $previousContent = $commentEnd; + } + + if ( + $tokens[$commentEnd]['code'] === T_ATTRIBUTE_END + && isset($tokens[$commentEnd]['attribute_opener']) === true + ) { + $commentEnd = $tokens[$commentEnd]['attribute_opener']; + continue; + } + + break; + } + + if ($commentEnd && $tokens[$commentEnd]['code'] === T_DOC_COMMENT_CLOSE_TAG) { + $opener = $tokens[$commentEnd]['comment_opener']; + + return $tokens[$opener]; + } + + return null; + } + + public static function getMatchingDocTags( + File $phpcsFile, + int $stackPtr, + string $tagName + ): array { + $tokens = $phpcsFile->getTokens(); + $docblock = self::getDocBlock($phpcsFile, $stackPtr); + if ($docblock === null) { + return []; + } + + $matchingTags = []; + foreach ($docblock['comment_tags'] as $tag) { + if ($tokens[$tag]['content'] === $tagName) { + $matchingTags[] = $tag; + } + } + + return $matchingTags; + } +} diff --git a/moodle/Util/MoodleUtil.php b/moodle/Util/MoodleUtil.php index 61a0daa..5842dd5 100644 --- a/moodle/Util/MoodleUtil.php +++ b/moodle/Util/MoodleUtil.php @@ -499,4 +499,22 @@ public static function findClassMethodPointer( return null; } + + /** + * Get all tokens relating to a particular line. + * + * @param File $phpcsFile + * @param int $line + * @return array + */ + public static function getTokensOnLine( + File $phpcsFile, + int $line + ): array { + return array_filter( + $phpcsFile->getTokens(), + fn($token) => $token['line'] === $line, + ARRAY_FILTER_USE_BOTH + ); + } }