Skip to content

Commit 979a494

Browse files
authored
Merge pull request #124 from andrewnicols/phpDocsTagsSniff
Introduce various doc tag sniffs
2 parents 4a519fe + 6b37c7e commit 979a494

21 files changed

+885
-67
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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\MoodleUtil;
21+
use MoodleHQ\MoodleCS\moodle\Util\Docblocks;
22+
use PHP_CodeSniffer\Sniffs\Sniff;
23+
use PHP_CodeSniffer\Files\File;
24+
25+
/**
26+
* Checks that valid docblock tags are in use.
27+
*
28+
* @copyright 2024 Andrew Lyons <[email protected]>
29+
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30+
*/
31+
class ValidTagsSniff implements Sniff
32+
{
33+
/**
34+
* Register for open tag (only process once per file).
35+
*/
36+
public function register() {
37+
return [
38+
T_OPEN_TAG,
39+
];
40+
}
41+
42+
/**
43+
* Processes php files and perform various checks with file.
44+
*
45+
* @param File $phpcsFile The file being scanned.
46+
* @param int $stackPtr The position in the stack.
47+
*/
48+
public function process(File $phpcsFile, $stackPtr) {
49+
$tokens = $phpcsFile->getTokens();
50+
51+
while ($docPtr = $phpcsFile->findNext(T_DOC_COMMENT_OPEN_TAG, $stackPtr)) {
52+
$docblock = Docblocks::getDocBlock($phpcsFile, $docPtr);
53+
foreach ($docblock['comment_tags'] as $tagPtr) {
54+
$tagName = ltrim($tokens[$tagPtr]['content'], '@');
55+
if (!Docblocks::isValidTag($phpcsFile, $tagPtr)) {
56+
if (Docblocks::shouldRemoveTag($tagName)) {
57+
$fix = $phpcsFile->addFixableError(
58+
'Invalid docblock tag "@%s" is not supported.',
59+
$tagPtr,
60+
'Invalid',
61+
[$tagName]
62+
);
63+
if ($fix) {
64+
$phpcsFile->fixer->beginChangeset();
65+
foreach ($this->getTokensOnTokenLine($phpcsFile, $tagPtr) as $tokenPtr) {
66+
$phpcsFile->fixer->replaceToken($tokenPtr, '');
67+
}
68+
$phpcsFile->fixer->endChangeset();
69+
}
70+
} elseif ($renameTo = Docblocks::getRenameTag($tagName)) {
71+
$fix = $phpcsFile->addFixableError(
72+
'Incorrect docblock tag "@%s". Should be "@%s".',
73+
$tagPtr,
74+
'Invalid',
75+
[$tagName, $renameTo]
76+
);
77+
if ($fix) {
78+
$phpcsFile->fixer->beginChangeset();
79+
$phpcsFile->fixer->replaceToken($tagPtr, "@{$renameTo}");
80+
$phpcsFile->fixer->endChangeset();
81+
}
82+
} else {
83+
$phpcsFile->addError(
84+
'Invalid docblock tag "@%s".',
85+
$tagPtr,
86+
'Invalid',
87+
[$tagName]
88+
);
89+
}
90+
} elseif (!Docblocks::isRecommendedTag($tagName)) {
91+
// The tag is valid, but not recommended.
92+
$phpcsFile->addWarning(
93+
'Docblock tag "@%s" is not recommended.',
94+
$tagPtr,
95+
'Invalid',
96+
[$tagName]
97+
);
98+
}
99+
}
100+
$stackPtr = $docPtr + 1;
101+
}
102+
}
103+
104+
/**
105+
* Get the tokens on the same line as the given token.
106+
*
107+
* @param File $phpcsFile
108+
* @param int $ptr
109+
* @return int[]
110+
*/
111+
protected function getTokensOnTokenLine(File $phpcsFile, int $ptr): array {
112+
$tokens = $phpcsFile->getTokens();
113+
$line = $tokens[$ptr]['line'];
114+
$lineTokens = [];
115+
for ($i = $ptr; $i >= 0; $i--) {
116+
if ($tokens[$i]['line'] === $line) {
117+
array_unshift($lineTokens, $i);
118+
continue;
119+
}
120+
break;
121+
}
122+
123+
$lineTokens[] = $ptr;
124+
125+
for ($i = $ptr; $i < count($tokens); $i++) {
126+
if ($tokens[$i]['line'] === $line) {
127+
$lineTokens[] = $i;
128+
continue;
129+
}
130+
break;
131+
}
132+
133+
return $lineTokens;
134+
}
135+
}

moodle/Tests/MoodleCSBaseTestCase.php

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ abstract class MoodleCSBaseTestCase extends \PHPUnit\Framework\TestCase
5858
*/
5959
protected ?string $fixture = null;
6060

61-
/** @var string|null Fixture file content */
62-
protected ?string $fixtureContent = null;
61+
/** @var string|null A path name to mock for the fixture */
62+
protected ?string $fixtureFileName = null;
6363

6464
/**
6565
* @var array custom config elements to setup before running phpcs. name => value.
@@ -88,6 +88,8 @@ protected function tearDown(): void {
8888
$this->sniff = null;
8989
$this->errors = null;
9090
$this->warnings = null;
91+
$this->fixture = null;
92+
$this->fixtureFileName = null;
9193
// Reset any mocked component mappings.
9294
\MoodleHQ\MoodleCS\moodle\Util\MoodleUtil::setMockedComponentMappings([]);
9395
// If there is any custom config setup, remove it.
@@ -144,22 +146,17 @@ protected function setSniff($sniff) {
144146
* Set the full path to the file used as input.
145147
*
146148
* @param string $fixture full path to the file used as input (fixture).
149+
* @param string|null $fileName A path name to mock for the fixture. If not specified, the fixture filepath is used.
147150
*/
148-
protected function setFixture($fixture) {
149-
if ($this->fixtureContent !== null) {
150-
$this->fail('Fixture file content already set, cannot set it again.');
151-
}
151+
protected function setFixture(
152+
string $fixture,
153+
?string $fileName = null
154+
) {
152155
if (!is_readable($fixture)) {
153156
$this->fail('Unreadable fixture passed: ' . $fixture);
154157
}
155158
$this->fixture = $fixture;
156-
}
157-
158-
protected function setFixtureFileContent(string $content): void {
159-
if ($this->fixture !== null) {
160-
$this->fail('Fixture file content already set, cannot set it again.');
161-
}
162-
$this->fixtureContent = $content;
159+
$this->fixtureFileName = $fileName;
163160
}
164161

165162
/**
@@ -223,8 +220,13 @@ protected function verifyCsResults() {
223220

224221
// Let's process the fixture.
225222
try {
226-
if ($this->fixtureContent !== null) {
227-
$phpcsfile = new \PHP_CodeSniffer\Files\DummyFile($this->fixtureContent, $ruleset, $config);
223+
if ($this->fixtureFileName !== null) {
224+
$fixtureSource = file_get_contents($this->fixture);
225+
$fixtureContent = <<<EOF
226+
phpcs_input_file: {$this->fixtureFileName}
227+
{$fixtureSource}
228+
EOF;
229+
$phpcsfile = new \PHP_CodeSniffer\Files\DummyFile($fixtureContent, $ruleset, $config);
228230
} else {
229231
$phpcsfile = new \PHP_CodeSniffer\Files\LocalFile($this->fixture, $ruleset, $config);
230232
}

moodle/Tests/Sniffs/Commenting/MissingDocblockSniffTest.php

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@ class MissingDocblockSniffTest extends MoodleCSBaseTestCase
3737
*/
3838
public function testMissingDocblockSniff(
3939
string $fixture,
40+
?string $fixtureFilename,
4041
array $errors,
4142
array $warnings
4243
): void {
4344
$this->setStandard('moodle');
4445
$this->setSniff('moodle.Commenting.MissingDocblock');
45-
$this->setFixture(sprintf("%s/fixtures/%s.php", __DIR__, $fixture));
46+
$this->setFixture(sprintf("%s/fixtures/MissingDocblock/%s.php", __DIR__, $fixture), $fixtureFilename);
4647
$this->setWarnings($warnings);
4748
$this->setErrors($errors);
4849
$this->setComponentMapping([
@@ -55,9 +56,10 @@ public function testMissingDocblockSniff(
5556
public static function docblockCorrectnessProvider(): array {
5657
$cases = [
5758
'Multiple artifacts in a file' => [
58-
'fixture' => 'missing_docblock_multiple_artifacts',
59+
'fixture' => 'multiple_artifacts',
60+
'fixtureFilename' => null,
5961
'errors' => [
60-
1 => 'Missing docblock for file missing_docblock_multiple_artifacts.php',
62+
1 => 'Missing docblock for file multiple_artifacts.php',
6163
34 => 'Missing docblock for function missing_docblock_in_function',
6264
38 => 'Missing docblock for class missing_docblock_in_class',
6365
95 => 'Missing docblock for interface missing_docblock_interface',
@@ -74,96 +76,80 @@ public static function docblockCorrectnessProvider(): array {
7476
],
7577
],
7678
'File level tag, no class' => [
77-
'fixture' => 'missing_docblock_class_without_docblock',
79+
'fixture' => 'class_without_docblock',
80+
'fixtureFilename' => null,
7881
'errors' => [
7982
11 => 'Missing docblock for class class_without_docblock',
8083
],
8184
'warnings' => [],
8285
],
8386
'Class only (incorrect whitespace)' => [
84-
'fixture' => 'missing_docblock_class_only_with_incorrect_whitespace',
87+
'fixture' => 'class_only_with_incorrect_whitespace',
88+
'fixtureFilename' => null,
8589
'errors' => [
8690
11 => 'Missing docblock for class class_only_with_incorrect_whitespace',
8791
],
8892
'warnings' => [],
8993
],
9094
'Class only (correct)' => [
91-
'fixture' => 'missing_docblock_class_only',
95+
'fixture' => 'class_only',
96+
'fixtureFilename' => null,
9297
'errors' => [],
9398
'warnings' => [],
9499
],
95100
'Class only with attributes (correct)' => [
96-
'fixture' => 'missing_docblock_class_only_with_attributes',
101+
'fixture' => 'class_only_with_attributes',
102+
'fixtureFilename' => null,
97103
'errors' => [],
98104
'warnings' => [],
99105
],
100106
'Class only with attributes and incorrect whitespace' => [
101-
'fixture' => 'missing_docblock_class_only_with_attributes_incorrect_whitespace',
107+
'fixture' => 'class_only_with_attributes_incorrect_whitespace',
108+
'fixtureFilename' => null,
102109
'errors' => [
103110
13 => 'Missing docblock for class class_only_with_attributes_incorrect_whitespace',
104111
],
105112
'warnings' => [],
106113
],
107114
'Class and file (correct)' => [
108-
'fixture' => 'missing_docblock_class_and_file',
115+
'fixture' => 'class_and_file',
116+
'fixtureFilename' => null,
109117
'errors' => [],
110118
'warnings' => [],
111119
],
112120
'Interface only (correct)' => [
113-
'fixture' => 'missing_docblock_interface_only',
121+
'fixture' => 'interface_only',
122+
'fixtureFilename' => null,
114123
'errors' => [],
115124
'warnings' => [],
116125
],
117126
'Trait only (correct)' => [
118-
'fixture' => 'missing_docblock_trait_only',
127+
'fixture' => 'trait_only',
128+
'fixtureFilename' => null,
119129
'errors' => [],
120130
'warnings' => [],
121131
],
132+
'Testcase' => [
133+
'fixture' => 'testcase_class',
134+
'fixtureFilename' => '/lib/tests/example_test.php',
135+
'errors' => [
136+
3 => 'Missing docblock for class example_test',
137+
],
138+
'warnings' => [
139+
12 => 'Missing docblock for function test_the_thing',
140+
],
141+
],
122142
];
123143

124144
if (version_compare(PHP_VERSION, '8.1.0') >= 0) {
125145
$cases['Enum only (correct)'] = [
126-
'fixture' => 'missing_docblock_enum_only',
146+
'fixture' => 'enum_only',
147+
'fixtureFilename' => null,
127148
'errors' => [],
128149
'warnings' => [],
129150
];
130151
}
131152

132153
return $cases;
133154
}
134-
135-
public function testMissingDocblockSniffWithTest(): void {
136-
$content = <<<EOF
137-
phpcs_input_file: /lib/tests/example_test.php
138-
<?php
139-
140-
class example_test extends advanced_testcase {
141-
public function setUp(): void {
142-
}
143-
public function tearDown(): void {
144-
}
145-
public static function setUpBeforeClass(): void {
146-
}
147-
public static function tearDownAfterClass(): void {
148-
}
149-
public function test_the_thing(): void {
150-
}
151-
}
152-
EOF;
153-
154-
$this->setStandard('moodle');
155-
$this->setSniff('moodle.Commenting.MissingDocblock');
156-
$this->setFixtureFileContent($content);
157-
$this->setWarnings([
158-
12 => 'Missing docblock for function test_the_thing',
159-
]);
160-
$this->setErrors([
161-
3 => 'Missing docblock for class example_test',
162-
]);
163-
$this->setComponentMapping([
164-
'local_codechecker' => dirname(__DIR__),
165-
]);
166-
167-
$this->verifyCsResults();
168-
}
169155
}

0 commit comments

Comments
 (0)