From a9a549a1e7800e8fe87986fbeb86cf1e1c885cfe Mon Sep 17 00:00:00 2001 From: Michael Aherne Date: Sun, 21 Apr 2024 16:47:00 +0100 Subject: [PATCH] Update BoilerplateCommentSniff and add fixes. --- .../Sniffs/Files/BoilerplateCommentSniff.php | 218 +++++++++++++++--- moodle/Tests/FilesBoilerPlateCommentTest.php | 52 ++++- .../boilerplatecomment/short_not_eof.php | 17 ++ .../trailing_whitespace.php | 17 ++ .../files/boilerplatecomment/wrong_place.php | 20 ++ 5 files changed, 287 insertions(+), 37 deletions(-) create mode 100644 moodle/Tests/fixtures/files/boilerplatecomment/short_not_eof.php create mode 100644 moodle/Tests/fixtures/files/boilerplatecomment/trailing_whitespace.php create mode 100644 moodle/Tests/fixtures/files/boilerplatecomment/wrong_place.php diff --git a/moodle/Sniffs/Files/BoilerplateCommentSniff.php b/moodle/Sniffs/Files/BoilerplateCommentSniff.php index 57efe20b..4b5f3df3 100644 --- a/moodle/Sniffs/Files/BoilerplateCommentSniff.php +++ b/moodle/Sniffs/Files/BoilerplateCommentSniff.php @@ -44,13 +44,20 @@ class BoilerplateCommentSniff implements Sniff "// 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 .", + "// along with Moodle. If not, see .", ]; - public function register() { + + public string $productName = 'Moodle'; + + public string $firstLinePostfix = ' - https://moodle.org/'; + + public function register() + { return [T_OPEN_TAG]; } - public function process(File $file, $stackptr) { + public function process(File $file, $stackptr) + { // We only want to do this once per file. $prevopentag = $file->findPrevious(T_OPEN_TAG, $stackptr - 1); if ($prevopentag !== false) { @@ -69,55 +76,198 @@ public function process(File $file, $stackptr) { $stackptr = $commentptr; } - // Find count the number of newlines after the opening findNext(T_COMMENT, $expectedafter + 1); + + // Check that it appears to be a Moodle boilerplate comment. + $regex = $this->regexForLine(self::$comment[0]); + $boilerplatefound = ($firstcommentptr !== false) && preg_match($regex, $tokens[$firstcommentptr]['content']); - if ($numnewlines > 0) { - $file->addError( - 'The opening addFixableError( + 'Moodle boilerplate not found', + $stackptr, + 'NoBoilerplateComment' ); + + if ($fix) { + $this->insertBoilerplate($file, $expectedafter); + } return; } - $offset = $stackptr + $numnewlines + 1; // Now check the text of the comment. + $textfixed = false; foreach (self::$comment as $lineindex => $line) { - $tokenptr = $offset + $lineindex; + // We already checked the first line. + if ($lineindex === 0) { + continue; + } + + $tokenptr = $firstcommentptr + $lineindex; + $iseof = $tokenptr >= $file->numTokens; + + if ($iseof || $tokens[$tokenptr]['code'] != T_COMMENT || strpos($tokens[$tokenptr]['content'], '//') !== 0) { + $errorline = $iseof ? $tokenptr - 1 : $tokenptr; + + $fix = $file->addFixableError( + 'Comment does not contain full Moodle boilerplate', + $errorline, + 'CommentEndedTooSoon' + ); + + if ($fix) { + $this->completeBoilerplate($file, $tokenptr - 1, $lineindex); + return; + } - if (!array_key_exists($tokenptr, $tokens)) { - $file->addError('Reached the end of the file before finding ' . - 'all of the opening comment.', $tokenptr - 1, 'FileTooShort'); + // No point checking whitespace after comment if it is incomplete. return; } - $regex = str_replace( - ['Moodle', 'http\\:'], - ['.*', 'https?\\:'], - '/^' . preg_quote($line, '/') . '/' - ); + $regex = $this->regexForLine($line); - if ( - $tokens[$tokenptr]['code'] != T_COMMENT || - !preg_match($regex, $tokens[$tokenptr]['content']) - ) { - $file->addError( + if (!preg_match($regex, $tokens[$tokenptr]['content'])) { + $fix = $file->addFixableError( 'Line %s of the opening comment must start "%s".', $tokenptr, 'WrongLine', [$lineindex + 1, $line] ); + + if ($fix) { + $file->fixer->replaceToken($tokenptr, $line . "\n"); + $textfixed = true; + } + } + } + + if ($firstcommentptr !== $expectedafter + 1) { + $fix = $file->addFixableError( + 'Moodle boilerplate not found at first line', + $expectedafter + 1, + 'NotAtFirstLine' + ); + + // If the boilerplate comment has been changed we need to commit the fixes before + // moving it. + if ($fix && !$textfixed) { + $this->moveBoilerplate($file, $firstcommentptr, $expectedafter); + } + + // There's no point in checking the whitespace after the boilerplate + // if it's not in the right place. + return; + } + + if ($tokenptr === $file->numTokens - 1) { + return; + } + + $tokenptr++; + + $nextnonwhitespace = $file->findNext(T_WHITESPACE, $tokenptr, null, true); + + // Allow indentation. + if ($nextnonwhitespace !== false && strpos($tokens[$nextnonwhitespace - 1]['content'], "\n") === false) { + $nextnonwhitespace--; + } + + if ( + ($nextnonwhitespace === false) && array_key_exists($tokenptr + 1, $tokens) + || ($nextnonwhitespace !== false && $nextnonwhitespace !== $tokenptr + 1) + ) { + $fix = $file->addFixableError( + 'Boilerplate comment must be followed by a single blank line or end of file', + $tokenptr, + 'SingleTrailingNewLine' + ); + + if ($fix) { + if ($nextnonwhitespace === false) { + while (array_key_exists(++$tokenptr, $tokens)) { + $file->fixer->replaceToken($tokenptr, ''); + } + } elseif ($nextnonwhitespace === $tokenptr) { + $file->fixer->addContentBefore($tokenptr, "\n"); + } else { + while (++$tokenptr < $nextnonwhitespace) { + if ($tokens[$tokenptr]['content'][-1] === "\n") { + $file->fixer->replaceToken($tokenptr, ''); + } + } + } + } + } + } + + private function fullComment(): array + { + $result = []; + foreach (self::$comment as $lineindex => $line) { + if ($lineindex === 0) { + $result[] = $line . ' ' . $this->productName . $this->firstLinePostfix; + } else { + $result[] = str_replace('Moodle', $this->productName, $line); + } + } + return $result; + } + + private function insertBoilerplate(File $file, int $stackptr): void + { + $prefix = substr($file->getTokens()[$stackptr]['content'], -1) === "\n" ? '' : "\n"; + $file->fixer->addContent($stackptr, $prefix . implode("\n", $this->fullComment()) . "\n"); + } + + private function moveBoilerplate(File $file, int $start, int $target): void + { + $tokens = $file->getTokens(); + + $file->fixer->beginChangeset(); + + // If we have only whitespace between expected location and first comment, just remove it. + $nextnonwhitespace = $file->findPrevious(T_WHITESPACE, $start - 1, $target, true); + + if ($nextnonwhitespace === false || $nextnonwhitespace === $target) { + foreach (range($target + 1, $start - 1) as $whitespaceptr) { + $file->fixer->replaceToken($whitespaceptr, ''); } + $file->fixer->endChangeset(); + return; + } + + // Otherwise shift existing comment to correct place. + $existingboilerplate = []; + foreach (range(0, count(self::$comment)) as $lineindex) { + $tokenptr = $start + $lineindex; + + $existingboilerplate[] = $tokens[$tokenptr]['content']; + + $file->fixer->replaceToken($tokenptr, ''); } + + $file->fixer->addContent($target, implode("", $existingboilerplate) . "\n"); + + $file->fixer->endChangeset(); + } + + private function completeBoilerplate(File $file, $stackptr, int $lineindex): void + { + $file->fixer->addContent($stackptr, implode("\n", array_slice($this->fullComment(), $lineindex)) . "\n"); + } + + /** + * @param string $line + * @return string + */ + private function regexForLine(string $line): string + { + return str_replace( + ['Moodle', 'https\\:'], + ['.*', 'https?\\:'], + '/^' . preg_quote($line, '/') . '/' + ); } } diff --git a/moodle/Tests/FilesBoilerPlateCommentTest.php b/moodle/Tests/FilesBoilerPlateCommentTest.php index b9fd94ba..508d0b96 100644 --- a/moodle/Tests/FilesBoilerPlateCommentTest.php +++ b/moodle/Tests/FilesBoilerPlateCommentTest.php @@ -69,7 +69,7 @@ public function testMoodleFilesBoilerplateCommentBlank() { $this->setFixture(__DIR__ . '/fixtures/files/boilerplatecomment/blank.php'); $this->setErrors([ - 2 => 'followed by exactly one newline', + 2 => 'not found at first line', ]); $this->setWarnings([]); @@ -82,7 +82,7 @@ public function testMoodleFilesBoilerplateCommentShort() { $this->setFixture(__DIR__ . '/fixtures/files/boilerplatecomment/short.php'); $this->setErrors([ - 14 => 'FileTooShort', + 14 => 'CommentEndedTooSoon', ]); $this->setWarnings([]); @@ -95,7 +95,20 @@ public function testMoodleFilesBoilerplateCommentShortEmpty() { $this->setFixture(__DIR__ . '/fixtures/files/boilerplatecomment/short_empty.php'); $this->setErrors([ - 1 => 'FileTooShort', + 1 => 'NoBoilerplateComment', + ]); + $this->setWarnings([]); + + $this->verifyCsResults(); + } + + public function testMoodleFilesBoilerplateCommentShortNotEof() { + $this->setStandard('moodle'); + $this->setSniff('moodle.Files.BoilerplateComment'); + $this->setFixture(__DIR__ . '/fixtures/files/boilerplatecomment/short_not_eof.php'); + + $this->setErrors([ + 15 => 'CommentEndedTooSoon', ]); $this->setWarnings([]); @@ -142,4 +155,37 @@ public function testMoodleFilesBoilerplateCommentGnuHttps() { $this->verifyCsResults(); } + + /** + * Assert that boilerplate is found if it is not the first thing in the file. + */ + public function testMoodleFilesBoilerplateCommentWrongPlace() { + $this->setStandard('moodle'); + $this->setSniff('moodle.Files.BoilerplateComment'); + $this->setFixture(__DIR__ . '/fixtures/files/boilerplatecomment/wrong_place.php'); + + $this->setErrors([ + 2 => 'not found at first line', + 9 => 'either version 3 of the License', + ]); + $this->setWarnings([]); + + $this->verifyCsResults(); + } + + /** + * Assert that boilerplate is followed by a single newline. + */ + public function testMoodleFilesBoilerplateCommentTrailingWhitespace() { + $this->setStandard('moodle'); + $this->setSniff('moodle.Files.BoilerplateComment'); + $this->setFixture(__DIR__ . '/fixtures/files/boilerplatecomment/trailing_whitespace.php'); + + $this->setErrors([ + 16 => 'SingleTrailingNewLine', + ]); + $this->setWarnings([]); + + $this->verifyCsResults(); + } } diff --git a/moodle/Tests/fixtures/files/boilerplatecomment/short_not_eof.php b/moodle/Tests/fixtures/files/boilerplatecomment/short_not_eof.php new file mode 100644 index 00000000..1ebf251b --- /dev/null +++ b/moodle/Tests/fixtures/files/boilerplatecomment/short_not_eof.php @@ -0,0 +1,17 @@ +. + + diff --git a/moodle/Tests/fixtures/files/boilerplatecomment/wrong_place.php b/moodle/Tests/fixtures/files/boilerplatecomment/wrong_place.php new file mode 100644 index 00000000..b35919fd --- /dev/null +++ b/moodle/Tests/fixtures/files/boilerplatecomment/wrong_place.php @@ -0,0 +1,20 @@ +. + +class someclass { } \ No newline at end of file