Skip to content

Commit

Permalink
Update BoilerplateCommentSniff and add fixes.
Browse files Browse the repository at this point in the history
  • Loading branch information
micaherne authored and stronk7 committed Jun 14, 2024
1 parent 22b99c9 commit a9a549a
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 37 deletions.
218 changes: 184 additions & 34 deletions moodle/Sniffs/Files/BoilerplateCommentSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://www.gnu.org/licenses/>.",
"// along with Moodle. If not, see <https://www.gnu.org/licenses/>.",
];
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) {
Expand All @@ -69,55 +76,198 @@ public function process(File $file, $stackptr) {
$stackptr = $commentptr;
}

// Find count the number of newlines after the opening <?PHP. We only
// count enough to see if the number is right.
// Note that the opening PHP tag includes one newline.
$numnewlines = 0;
for ($i = $stackptr + 1; $i <= $stackptr + 5; ++$i) {
if (isset($tokens[$i]) && $tokens[$i]['code'] == T_WHITESPACE && $tokens[$i]['content'] == "\n") {
$numnewlines++;
} else {
break;
}
}
$expectedafter = $stackptr;

$firstcommentptr = $file->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 <?php tag must be followed by exactly one newline.',
$stackptr + 1,
'WrongWhitespace'
if (!$boilerplatefound) {
$fix = $file->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--;

Check warning on line 174 in moodle/Sniffs/Files/BoilerplateCommentSniff.php

View check run for this annotation

Codecov / codecov/patch

moodle/Sniffs/Files/BoilerplateCommentSniff.php#L174

Added line #L174 was not covered by tests
}

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");

Check warning on line 193 in moodle/Sniffs/Files/BoilerplateCommentSniff.php

View check run for this annotation

Codecov / codecov/patch

moodle/Sniffs/Files/BoilerplateCommentSniff.php#L193

Added line #L193 was not covered by tests
} 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, '/') . '/'
);
}
}
52 changes: 49 additions & 3 deletions moodle/Tests/FilesBoilerPlateCommentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);

Expand All @@ -82,7 +82,7 @@ public function testMoodleFilesBoilerplateCommentShort() {
$this->setFixture(__DIR__ . '/fixtures/files/boilerplatecomment/short.php');

$this->setErrors([
14 => 'FileTooShort',
14 => 'CommentEndedTooSoon',
]);
$this->setWarnings([]);

Expand All @@ -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([]);

Expand Down Expand Up @@ -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();
}
}
17 changes: 17 additions & 0 deletions moodle/Tests/fixtures/files/boilerplatecomment/short_not_eof.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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

class some_class {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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/>.


20 changes: 20 additions & 0 deletions moodle/Tests/fixtures/files/boilerplatecomment/wrong_place.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

use core\context\system;

// 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 N 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/>.

class someclass { }

0 comments on commit a9a549a

Please sign in to comment.