diff --git a/moodle/Sniffs/Commenting/FileHasSniff.php b/moodle/Sniffs/Commenting/FileHasSniff.php index 73e46d6..1a5ff9f 100644 --- a/moodle/Sniffs/Commenting/FileHasSniff.php +++ b/moodle/Sniffs/Commenting/FileHasSniff.php @@ -19,8 +19,11 @@ use MoodleHQ\MoodleCS\moodle\Util\Tokens; use MoodleHQ\MoodleCS\moodle\Util\Docblocks; +use MoodleHQ\MoodleCS\moodle\Util\TokenUtil; use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Files\File; +use PHPCSUtils\Tokens\Collections; +use phpDocumentor\Reflection\Types\Collection; /** * Checks that a file has appropriate tags in either the file, or single artefact block. @@ -35,7 +38,7 @@ class FileHasSniff implements Sniff */ public function register() { return [ - T_DOC_COMMENT_OPEN_TAG, + T_OPEN_TAG, ]; } @@ -46,45 +49,99 @@ public function register() { * @param int $stackPtr The position in the stack. */ public function process(File $phpcsFile, $stackPtr) { + // Get the stack pointer for the file-level docblock. + $stackPtr = Docblocks::getDocBlockPointer($phpcsFile, $stackPtr); + if ($stackPtr === null) { + // There is no file-level docblock. + + if (TokenUtil::countGlobalScopesInFile($phpcsFile) > 1) { + // There are more than one item in the global scope. + // Only accept the file docblock. + return; + } else { + // There is only one item in the global scope. + // We can accept the file docblock or the item docblock. + $stackPtr = $phpcsFile->findNext(Collections::closedScopes(), 0); + } + } + $this->processFileCopyright($phpcsFile, $stackPtr); + $this->processFileLicense($phpcsFile, $stackPtr); } + /** + * Process the file docblock and check for the presence of a @copyright tag. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack. + */ private function processFileCopyright(File $phpcsFile, $stackPtr): void { $copyrightTokens = Docblocks::getMatchingDocTags($phpcsFile, $stackPtr, '@copyright'); if (empty($copyrightTokens)) { $phpcsFile->addError( 'Missing @copyright tag', $stackPtr, - 'Missing' + 'CopyrightTagMissing' ); return; } + } - $docblockPtr = Docblocks::getDocBlockPointer($phpcsFile, $stackPtr); - $objectType = Tokens::getObjectType($phpcsFile, $docblockPtr); - $find = [ - T_CLASS, - T_FUNCTION, - T_ENUM, - T_INTERFACE, - T_TRAIT, - T_OPEN_TAG, - ]; - $owner = $phpcsFile->findPrevious($find, $docblockPtr); - var_dump($docblockPtr); - var_dump($objectType); - if ($docblockPtr && $objectType === 'file') { - $copyrightTokens = Docblocks::getMatchingDocTags($phpcsFile, $stackPtr, '@copyright'); - if (count($copyrightTokens) === 0) { - $phpcsFile->addError( - 'Missing @copyright tag in file docblock', - $stackPtr, - 'Missing' - ); - } + /** + * Process the file docblock and check for the presence of a @license tag. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack. + */ + private function processFileLicense(File $phpcsFile, $stackPtr): void { + $foundTokens = Docblocks::getMatchingDocTags($phpcsFile, $stackPtr, '@license'); + if (empty($foundTokens)) { + $fix = $phpcsFile->addFixableError( + 'Missing @license tag', + $stackPtr, + 'LicenseTagMissing' + ); + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + // Check for any existing doc. + $docPtr = Docblocks::getDocBlockPointer($phpcsFile, $stackPtr); + if ($docPtr) { + $tokens = $phpcsFile->getTokens(); + $endPtr = $tokens[$docPtr]['comment_closer']; + $phpcsFile->fixer->addNewlineBefore($endPtr - 1); + $phpcsFile->fixer->addContentBefore($endPtr - 1, ' * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later'); + } else { + $newDocblock = <<fixer->addContentBefore($stackPtr, $newDocblock); + } + $phpcsFile->fixer->endChangeset(); + } return; } + + $tokens = $phpcsFile->getTokens(); + + $licensePtr = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $foundTokens[0]); + $license = $tokens[$licensePtr]['content']; + + $expected = 'https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later'; + if ($license !== $expected) { + $fix = $phpcsFile->addFixableWarning( + 'Invalid @license tag. Expected "%s", found "%s"', + $licensePtr, + 'LicenseTagInvalid', + [$expected, $license] + ); + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($licensePtr, $expected); + $phpcsFile->fixer->endChangeset(); + } + } } } diff --git a/moodle/Tests/Sniffs/Commenting/FileHasSniffTest.php b/moodle/Tests/Sniffs/Commenting/FileHasSniffTest.php new file mode 100644 index 0000000..4447a27 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/FileHasSniffTest.php @@ -0,0 +1,160 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Tests\Sniffs\Commenting; + +use MoodleHQ\MoodleCS\moodle\Tests\MoodleCSBaseTestCase; + +/** + * Test the VariableCommentSniff sniff. + * + * @copyright 2024 onwards Andrew Lyons + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\Commenting\FileHasSniff + */ +class FileHasSniffTest extends MoodleCSBaseTestCase +{ + /** + * @dataProvider fixtureProvider + */ + public function testSniffWithFixtures( + string $fixture, + array $errors, + array $warnings + ): void { + $this->setStandard('moodle'); + $this->setSniff('moodle.Commenting.FileHas'); + $this->setFixture(sprintf("%s/fixtures/FileHas/%s.php", __DIR__, $fixture)); + $this->setErrors($errors); + $this->setWarnings($warnings); + + $this->verifyCsResults(); + } + + public static function fixtureProvider(): array { + $cases = [ + 'Single artifact, Single docblock' => [ + 'fixture' => 'single_artifact_single_docblock', + 'errors' => [], + 'warnings' => [], + ], + 'Single artifact, Single docblock (missing)' => [ + 'fixture' => 'single_artifact_single_docblock_missing', + 'errors' => [ + 3 => [ + 'Missing @copyright tag', + 'Missing @license tag', + ], + ], + 'warnings' => [], + ], + 'Single artifact, Single docblock (missing tags)' => [ + 'fixture' => 'single_artifact_single_docblock_missing_tags', + 'errors' => [ + 6 => [ + 'Missing @copyright tag', + 'Missing @license tag', + ], + ], + 'warnings' => [], + ], + 'Single artifact, multiple docblocks' => [ + 'fixture' => 'single_artifact_multiple_docblock', + 'errors' => [ + ], + 'warnings' => [], + ], + 'Single artifact, multiple docblocks (missing)' => [ + 'fixture' => 'single_artifact_multiple_docblock_missing', + 'errors' => [ + // Note: Covered by MissingDocblockSniff. + ], + 'warnings' => [], + ], + 'Single artifact, multiple docblocks (wrong)' => [ + 'fixture' => 'single_artifact_multiple_docblock_wrong', + 'errors' => [ + ], + 'warnings' => [ + 7 => 'Invalid @license tag. Expected "https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later", found "http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later"', + ], + ], + 'Multiple artifacts, File docblock' => [ + 'fixture' => 'multiple_artifact_has_file_docblock', + 'errors' => [], + 'warnings' => [], + ], + 'Multiple artifacts, File docblock (missing)' => [ + 'fixture' => 'multiple_artifact_has_file_docblock_missing', + 'errors' => [], + 'warnings' => [], + ], + 'Multiple artifacts, File docblock (wrong data)' => [ + 'fixture' => 'multiple_artifact_has_file_docblock_wrong', + 'errors' => [ + 3 => 'Missing @copyright tag', + ], + 'warnings' => [ + 6 => 'Invalid @license tag. Expected "https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later", found "Me!"', + ], + ], + 'Multiple artifacts, File docblock (missing tags)' => [ + 'fixture' => 'multiple_artifact_has_file_docblock_missing_tags', + 'errors' => [ + 3 => [ + 'Missing @copyright tag', + 'Missing @license tag', + ], + ], + 'warnings' => [], + ], + ]; + + // if (version_compare(PHP_VERSION, '8.0.0') >= 0) { + // // Since PHP 8.0. + // $cases['Constructor Property Promotions'] = [ + // 'fixture' => 'constructor_property_promotion', + // 'errors' => [ + // 7 => 'Missing member variable doc comment', + // 13 => 'Expected "bool" but found "BOOLEAN" for @var tag in member variable comment', + // 20 => 'The @var tag must be the first tag in a member variable commen', + // ], + // 'warnings' => [ + // 19 => '@deprecated tag is not allowed in member variable comment', + // ], + // ]; + // } + + // if (version_compare(PHP_VERSION, '8.1.0') >= 0) { + // // Since PHP 8.1. + // $cases['Constructor Property Promotions with Readonly params'] = [ + // 'fixture' => 'constructor_property_promotion_readonly', + // 'errors' => [ + // 7 => 'Missing member variable doc comment', + // 13 => 'Expected "bool" but found "BOOLEAN" for @var tag in member variable comment', + // 20 => 'The @var tag must be the first tag in a member variable commen', + // ], + // 'warnings' => [ + // 19 => '@deprecated tag is not allowed in member variable comment', + // ], + // ]; + // } + + return $cases; + } +} diff --git a/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/multiple_artifact_has_file_docblock.php b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/multiple_artifact_has_file_docblock.php new file mode 100644 index 0000000..8a70c47 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/multiple_artifact_has_file_docblock.php @@ -0,0 +1,23 @@ + + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Class docblock. + * + */ +class multiple_artifact_has_file_docblock +{ + /** + * @var string + */ + private string $name; +} + +trait example_trait {} +interface example_interface {} diff --git a/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/multiple_artifact_has_file_docblock_missing.php b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/multiple_artifact_has_file_docblock_missing.php new file mode 100644 index 0000000..74acb60 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/multiple_artifact_has_file_docblock_missing.php @@ -0,0 +1,16 @@ + + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Class docblock. + */ +class single_artifact_multiple_docblock +{ + /** + * @var string + */ + private string $name; +} diff --git a/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_multiple_docblock_missing.php b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_multiple_docblock_missing.php new file mode 100644 index 0000000..1553104 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_multiple_docblock_missing.php @@ -0,0 +1,17 @@ + + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Class docblock. + */ +class single_artifact_multiple_docblock_missing +{ + /** + * @var string + */ + private string $name; +} diff --git a/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_multiple_docblock_wrong.php.fixed b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_multiple_docblock_wrong.php.fixed new file mode 100644 index 0000000..1b5ba54 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_multiple_docblock_wrong.php.fixed @@ -0,0 +1,19 @@ + + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Class docblock. + */ +class single_artifact_multiple_docblock_missing +{ + /** + * @var string + */ + private string $name; +} diff --git a/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_single_docblock.php b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_single_docblock.php new file mode 100644 index 0000000..2c73ea1 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_single_docblock.php @@ -0,0 +1,15 @@ + + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class single_class_single_docblock +{ + /** + * @var string + */ + private string $name; +} diff --git a/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_single_docblock_missing.php b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_single_docblock_missing.php new file mode 100644 index 0000000..10be8d3 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/fixtures/FileHas/single_artifact_single_docblock_missing.php @@ -0,0 +1,9 @@ +