Skip to content

Commit 7c207d5

Browse files
authored
Merge pull request #125 from andrewnicols/fileHasSniff
Add sniffs for file docblock @copyright and @license
2 parents 979a494 + f136673 commit 7c207d5

15 files changed

+597
-0
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
3+
// This file is part of Moodle - https://moodle.org/
4+
//
5+
// Moodle is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// Moodle is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
17+
18+
namespace MoodleHQ\MoodleCS\moodle\Sniffs\Commenting;
19+
20+
use MoodleHQ\MoodleCS\moodle\Util\Docblocks;
21+
use MoodleHQ\MoodleCS\moodle\Util\TokenUtil;
22+
use PHP_CodeSniffer\Config;
23+
use PHP_CodeSniffer\Sniffs\Sniff;
24+
use PHP_CodeSniffer\Files\File;
25+
use PHPCSUtils\Tokens\Collections;
26+
27+
/**
28+
* Checks that a file has appropriate tags in either the file, or single artefact block.
29+
*
30+
* @copyright 2024 Andrew Lyons <[email protected]>
31+
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32+
*/
33+
class FileExpectedTagsSniff implements Sniff
34+
{
35+
/**
36+
* The regular expression used to match the expected license.
37+
*
38+
* Note that the regular expression is applied using preg_quote to escape as required.
39+
*
40+
* Note that, if the regular expression is the empty string,
41+
* then this Sniff will do nothing.
42+
*
43+
* Example values:
44+
* - Empty string or null: No check is done.
45+
* ''
46+
* - The GNU GPL v3 or later license with either http or https license text
47+
* '@https?://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later@':
48+
*
49+
* @var null|string
50+
*/
51+
public ?string $preferredLicenseRegex = '@https?://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later@';
52+
53+
/**
54+
* Register for open tag (only process once per file).
55+
*/
56+
public function register() {
57+
return [
58+
T_OPEN_TAG,
59+
];
60+
}
61+
62+
/**
63+
* Processes php files and perform various checks with file.
64+
*
65+
* @param File $phpcsFile The file being scanned.
66+
* @param int $stackPtr The position in the stack.
67+
*/
68+
public function process(File $phpcsFile, $stackPtr) {
69+
// Get the stack pointer for the file-level docblock.
70+
$stackPtr = Docblocks::getDocBlockPointer($phpcsFile, $stackPtr);
71+
if ($stackPtr === null) {
72+
// There is no file-level docblock.
73+
74+
if (TokenUtil::countGlobalScopesInFile($phpcsFile) > 1) {
75+
// There are more than one item in the global scope.
76+
// Only accept the file docblock.
77+
return;
78+
} else {
79+
// There is only one item in the global scope.
80+
// We can accept the file docblock or the item docblock.
81+
$stackPtr = $phpcsFile->findNext(Collections::closedScopes(), 0);
82+
}
83+
}
84+
85+
$this->processFileCopyright($phpcsFile, $stackPtr);
86+
$this->processFileLicense($phpcsFile, $stackPtr);
87+
}
88+
89+
/**
90+
* Process the file docblock and check for the presence of a @copyright tag.
91+
*
92+
* @param File $phpcsFile The file being scanned.
93+
* @param int $stackPtr The position in the stack.
94+
*/
95+
private function processFileCopyright(File $phpcsFile, $stackPtr): void {
96+
$copyrightTokens = Docblocks::getMatchingDocTags($phpcsFile, $stackPtr, '@copyright');
97+
if (empty($copyrightTokens)) {
98+
$docPtr = Docblocks::getDocBlockPointer($phpcsFile, $stackPtr);
99+
if (empty($docPtr)) {
100+
$docPtr = $stackPtr;
101+
}
102+
103+
$phpcsFile->addError(
104+
'Missing @copyright tag',
105+
$docPtr,
106+
'CopyrightTagMissing'
107+
);
108+
return;
109+
}
110+
}
111+
112+
/**
113+
* Process the file docblock and check for the presence of a @license tag.
114+
*
115+
* @param File $phpcsFile The file being scanned.
116+
* @param int $stackPtr The position in the stack.
117+
*/
118+
private function processFileLicense(File $phpcsFile, $stackPtr): void {
119+
$tokens = $phpcsFile->getTokens();
120+
$foundTokens = Docblocks::getMatchingDocTags($phpcsFile, $stackPtr, '@license');
121+
if (empty($foundTokens)) {
122+
$docPtr = Docblocks::getDocBlockPointer($phpcsFile, $stackPtr);
123+
if ($docPtr) {
124+
$phpcsFile->addError(
125+
'Missing @license tag',
126+
$docPtr,
127+
'LicenseTagMissing'
128+
);
129+
} else {
130+
$phpcsFile->addError(
131+
'Missing @license tag',
132+
$stackPtr,
133+
'LicenseTagMissing'
134+
);
135+
}
136+
return;
137+
}
138+
139+
// If specified, get the regular expression from the config.
140+
if (($regex = Config::getConfigData('moodleLicenseRegex')) !== null) {
141+
$this->preferredLicenseRegex = $regex;
142+
}
143+
144+
if ($this->preferredLicenseRegex === '') {
145+
return;
146+
}
147+
148+
$licensePtr = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $foundTokens[0]);
149+
$license = $tokens[$licensePtr]['content'];
150+
151+
if (!preg_match($this->preferredLicenseRegex, $license)) {
152+
$phpcsFile->addWarning(
153+
'Invalid @license tag. Value "%s" does not match expected format',
154+
$licensePtr,
155+
'LicenseTagInvalid',
156+
[$license]
157+
);
158+
}
159+
}
160+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
// This file is part of Moodle - https://moodle.org/
4+
//
5+
// Moodle is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// Moodle is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
17+
18+
namespace MoodleHQ\MoodleCS\moodle\Tests\Sniffs\Commenting;
19+
20+
use MoodleHQ\MoodleCS\moodle\Tests\MoodleCSBaseTestCase;
21+
22+
/**
23+
* Test the VariableCommentSniff sniff.
24+
*
25+
* @copyright 2024 onwards Andrew Lyons <[email protected]>
26+
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27+
*
28+
* @covers \MoodleHQ\MoodleCS\moodle\Sniffs\Commenting\FileExpectedTagsSniff
29+
*/
30+
class FileExpectedTagsSniffTest extends MoodleCSBaseTestCase
31+
{
32+
/**
33+
* @dataProvider fixtureProvider
34+
*/
35+
public function testSniffWithFixtures(
36+
string $fixture,
37+
array $errors,
38+
array $warnings,
39+
array $configValues
40+
): void {
41+
$this->setStandard('moodle');
42+
$this->setSniff('moodle.Commenting.FileExpectedTags');
43+
$this->setFixture(sprintf("%s/fixtures/FileExpectedTags/%s.php", __DIR__, $fixture));
44+
$this->setErrors($errors);
45+
$this->setWarnings($warnings);
46+
47+
foreach ($configValues as $configKey => $configValue) {
48+
$this->addCustomConfig($configKey, $configValue);
49+
}
50+
51+
$this->verifyCsResults();
52+
}
53+
54+
public static function fixtureProvider(): array {
55+
$cases = [
56+
'Single artifact, Single docblock' => [
57+
'fixture' => 'single_artifact_single_docblock',
58+
'errors' => [],
59+
'warnings' => [],
60+
'configValues' => [],
61+
],
62+
'Single artifact, Single docblock (missing)' => [
63+
'fixture' => 'single_artifact_single_docblock_missing',
64+
'errors' => [
65+
3 => [
66+
'Missing @copyright tag',
67+
'Missing @license tag',
68+
],
69+
],
70+
'warnings' => [],
71+
'configValues' => [],
72+
],
73+
'Single artifact, Single docblock (missing tags)' => [
74+
'fixture' => 'single_artifact_single_docblock_missing_tags',
75+
'errors' => [
76+
3 => [
77+
'Missing @copyright tag',
78+
'Missing @license tag',
79+
],
80+
],
81+
'warnings' => [],
82+
'configValues' => [],
83+
],
84+
'Single artifact, multiple docblocks' => [
85+
'fixture' => 'single_artifact_multiple_docblock',
86+
'errors' => [
87+
],
88+
'warnings' => [],
89+
'configValues' => [],
90+
],
91+
'Single artifact, multiple docblocks (missing)' => [
92+
'fixture' => 'single_artifact_multiple_docblock_missing',
93+
'errors' => [
94+
// Note: Covered by MissingDocblockSniff.
95+
],
96+
'warnings' => [],
97+
'configValues' => [],
98+
],
99+
'Single artifact, multiple docblocks (wrong)' => [
100+
'fixture' => 'single_artifact_multiple_docblock_wrong',
101+
'errors' => [],
102+
'warnings' => [],
103+
'configValues' => [],
104+
],
105+
'Multiple artifacts, File docblock' => [
106+
'fixture' => 'multiple_artifact_has_file_docblock',
107+
'errors' => [],
108+
'warnings' => [],
109+
'configValues' => [],
110+
],
111+
'Multiple artifacts, File docblock (missing)' => [
112+
'fixture' => 'multiple_artifact_has_file_docblock_missing',
113+
'errors' => [],
114+
'warnings' => [],
115+
'configValues' => [],
116+
],
117+
'Multiple artifacts, File docblock (wrong data)' => [
118+
'fixture' => 'multiple_artifact_has_file_docblock_wrong',
119+
'errors' => [
120+
3 => 'Missing @copyright tag',
121+
],
122+
'warnings' => [
123+
6 => 'Invalid @license tag. Value "Me!" does not match expected format',
124+
],
125+
'configValues' => [],
126+
],
127+
'Multiple artifacts, File docblock, Custom license value set' => [
128+
'fixture' => 'multiple_artifact_has_file_docblock_wrong',
129+
'errors' => [
130+
3 => 'Missing @copyright tag',
131+
],
132+
'warnings' => [],
133+
'configValues' => [
134+
'moodleLicenseRegex' => '@Me!@',
135+
],
136+
],
137+
'Multiple artifacts, File docblock, No license value set' => [
138+
'fixture' => 'multiple_artifact_has_file_docblock_wrong',
139+
'errors' => [
140+
3 => 'Missing @copyright tag',
141+
],
142+
'warnings' => [],
143+
'configValues' => [
144+
'moodleLicenseRegex' => '',
145+
],
146+
],
147+
'Multiple artifacts, File docblock (missing tags)' => [
148+
'fixture' => 'multiple_artifact_has_file_docblock_missing_tags',
149+
'errors' => [
150+
3 => [
151+
'Missing @copyright tag',
152+
'Missing @license tag',
153+
],
154+
],
155+
'warnings' => [],
156+
'configValues' => [],
157+
],
158+
];
159+
160+
return $cases;
161+
}
162+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/**
4+
* File docblock
5+
*
6+
* @copyright 2024 Andrew Lyons <[email protected]>
7+
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
8+
*/
9+
10+
/**
11+
* Class docblock.
12+
*
13+
*/
14+
class multiple_artifact_has_file_docblock
15+
{
16+
/**
17+
* @var string
18+
*/
19+
private string $name;
20+
}
21+
22+
trait example_trait {}
23+
interface example_interface {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/**
4+
* Class docblock.
5+
*
6+
*/
7+
class multiple_artifact_has_file_docblock_missing
8+
{
9+
/**
10+
* @var string
11+
*/
12+
private string $name;
13+
}
14+
15+
trait example_trait {}
16+
interface example_interface {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/**
4+
* File docblock
5+
*/
6+
7+
/**
8+
* Class docblock.
9+
*
10+
*/
11+
class multiple_artifact_has_file_docblock_missing_tags
12+
{
13+
/**
14+
* @var string
15+
*/
16+
private string $name;
17+
}
18+
19+
trait example_trait {}
20+
interface example_interface {}

0 commit comments

Comments
 (0)