Skip to content

Commit 24bc48a

Browse files
authored
Merge pull request #121 from andrewnicols/varSniff
Add sniff to cover correct use of @var
2 parents 7c207d5 + f4c4202 commit 24bc48a

13 files changed

+906
-1
lines changed

moodle/Sniffs/Commenting/TodoCommentSniff.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class TodoCommentSniff implements Sniff
5555
/**
5656
* Returns an array of tokens this Sniff wants to listen for.
5757
*
58-
* @return array<int|string>
58+
* @return int[]|string[]
5959
*/
6060
public function register(): array {
6161
return [T_COMMENT, T_DOC_COMMENT_TAG];
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
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 WARRANdTY; 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 PHP_CodeSniffer\Files\File;
21+
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff;
22+
use PHPCSUtils\Tokens\Collections;
23+
use PHPCSUtils\Utils\ObjectDeclarations;
24+
25+
/**
26+
* Parses and verifies the variable doc comment.
27+
*
28+
* The Sniff is based upon the Squiz Labs version, but it has been modified to accept int, rather than integer.
29+
*
30+
* @author Greg Sherwood <[email protected]>
31+
* @author Andrew Lyons <[email protected]>
32+
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
33+
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
34+
*/
35+
class VariableCommentSniff extends AbstractVariableSniff
36+
{
37+
/**
38+
* An array of variable types for param/var we will check.
39+
*
40+
* @var string[]
41+
*/
42+
protected static $allowedTypes = [
43+
'array',
44+
'bool',
45+
'float',
46+
'int',
47+
'mixed',
48+
'object',
49+
'string',
50+
'resource',
51+
'callable',
52+
];
53+
54+
/**
55+
* Called to process class member vars.
56+
*
57+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
58+
* @param int $stackPtr The position of the current token
59+
* in the stack passed in $tokens.
60+
*
61+
* @return void
62+
*/
63+
public function processMemberVar(File $phpcsFile, $stackPtr) {
64+
$tokens = $phpcsFile->getTokens();
65+
66+
$ignore = [
67+
T_WHITESPACE => T_WHITESPACE,
68+
T_NULLABLE => T_NULLABLE,
69+
]
70+
+ Collections::propertyModifierKeywords()
71+
+ Collections::parameterTypeTokens();
72+
73+
for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) {
74+
if (isset($ignore[$tokens[$commentEnd]['code']]) === true) {
75+
continue;
76+
}
77+
78+
if (
79+
$tokens[$commentEnd]['code'] === T_ATTRIBUTE_END
80+
&& isset($tokens[$commentEnd]['attribute_opener']) === true
81+
) {
82+
$commentEnd = $tokens[$commentEnd]['attribute_opener'];
83+
continue;
84+
}
85+
86+
break;
87+
}
88+
89+
if (
90+
$commentEnd === false
91+
|| ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
92+
&& $tokens[$commentEnd]['code'] !== T_COMMENT)
93+
) {
94+
$phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing');
95+
return;
96+
}
97+
98+
if ($tokens[$commentEnd]['code'] === T_COMMENT) {
99+
$phpcsFile->addError('You must use "/**" style comments for a member variable comment', $stackPtr, 'WrongStyle');
100+
return;
101+
}
102+
103+
$commentStart = $tokens[$commentEnd]['comment_opener'];
104+
105+
$foundVar = null;
106+
foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
107+
if ($tokens[$tag]['content'] === '@var') {
108+
if ($foundVar !== null) {
109+
$error = 'Only one @var tag is allowed in a member variable comment';
110+
$phpcsFile->addError($error, $tag, 'DuplicateVar');
111+
} else {
112+
$foundVar = $tag;
113+
}
114+
} elseif ($tokens[$tag]['content'] === '@see') {
115+
// Make sure the tag isn't empty.
116+
$string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
117+
if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
118+
$error = 'Content missing for @see tag in member variable comment';
119+
$phpcsFile->addError($error, $tag, 'EmptySees');
120+
}
121+
} else {
122+
$error = '%s tag is not allowed in member variable comment';
123+
$data = [$tokens[$tag]['content']];
124+
$phpcsFile->addWarning($error, $tag, 'TagNotAllowed', $data);
125+
}
126+
}
127+
128+
// The @var tag is the only one we require.
129+
if ($foundVar === null) {
130+
$error = 'Missing @var tag in member variable comment';
131+
$phpcsFile->addError($error, $commentEnd, 'MissingVar');
132+
return;
133+
}
134+
135+
$firstTag = $tokens[$commentStart]['comment_tags'][0];
136+
if ($foundVar !== null && $tokens[$firstTag]['content'] !== '@var') {
137+
$error = 'The @var tag must be the first tag in a member variable comment';
138+
$phpcsFile->addError($error, $foundVar, 'VarOrder');
139+
}
140+
141+
// Make sure the tag isn't empty and has the correct padding.
142+
$string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $foundVar, $commentEnd);
143+
if ($string === false || $tokens[$string]['line'] !== $tokens[$foundVar]['line']) {
144+
$error = 'Content missing for @var tag in member variable comment';
145+
$phpcsFile->addError($error, $foundVar, 'EmptyVar');
146+
return;
147+
}
148+
149+
// Support both a var type and a description.
150+
preg_match('`^((?:\|?(?:array\([^\)]*\)|[\\\\a-z0-9\[\]]+))*)( .*)?`i', $tokens[($foundVar + 2)]['content'], $varParts);
151+
$varType = $varParts[1];
152+
153+
// Check var type (can be multiple, separated by '|').
154+
$typeNames = explode('|', $varType);
155+
$suggestedNames = [];
156+
foreach ($typeNames as $i => $typeName) {
157+
$suggestedName = self::suggestType($typeName);
158+
if (in_array($suggestedName, $suggestedNames, true) === false) {
159+
$suggestedNames[] = $suggestedName;
160+
}
161+
}
162+
163+
$suggestedType = implode('|', $suggestedNames);
164+
if ($varType !== $suggestedType) {
165+
$error = 'Expected "%s" but found "%s" for @var tag in member variable comment';
166+
$data = [
167+
$suggestedType,
168+
$varType,
169+
];
170+
171+
$fix = $phpcsFile->addFixableError($error, $foundVar, 'IncorrectVarType', $data);
172+
if ($fix === true) {
173+
$replacement = $suggestedType;
174+
if (empty($varParts[2]) === false) {
175+
$replacement .= $varParts[2];
176+
}
177+
178+
$phpcsFile->fixer->replaceToken(($foundVar + 2), $replacement);
179+
unset($replacement);
180+
}
181+
}
182+
}
183+
184+
/**
185+
* Processes normal variables within a method.
186+
*
187+
* @param File $file The file where this token was found.
188+
* @param int $stackptr The position where the token was found.
189+
*
190+
* @return void
191+
*/
192+
protected function processVariable(File $phpcsFile, $stackPtr) {
193+
// Find the method that this variable is declared in.
194+
$methodPtr = $phpcsFile->findPrevious(T_FUNCTION, $stackPtr);
195+
if ($methodPtr === false) {
196+
// Not in a method.
197+
return; // @codeCoverageIgnore
198+
}
199+
200+
$methodName = ObjectDeclarations::getName($phpcsFile, $methodPtr);
201+
if ($methodName !== '__construct') {
202+
// Not in a constructor.
203+
return;
204+
}
205+
206+
$method = $phpcsFile->getTokens()[$methodPtr];
207+
if ($method['parenthesis_opener'] < $stackPtr && $method['parenthesis_closer'] > $stackPtr) {
208+
$this->processMemberVar($phpcsFile, $stackPtr);
209+
return;
210+
}
211+
}
212+
213+
/**
214+
* Returns a valid variable type for param/var tags.
215+
*
216+
* If type is not one of the standard types, it must be a custom type.
217+
* Returns the correct type name suggestion if type name is invalid.
218+
*
219+
* @param string $varType The variable type to process.
220+
*
221+
* @return string
222+
*/
223+
protected static function suggestType(string $varType): string {
224+
if (in_array($varType, self::$allowedTypes, true) === true) {
225+
return $varType;
226+
} elseif (substr($varType, -2) === '[]') {
227+
return sprintf(
228+
'%s[]',
229+
self::suggestType(substr($varType, 0, -2))
230+
);
231+
} else {
232+
$lowerVarType = strtolower($varType);
233+
switch ($lowerVarType) {
234+
case 'bool':
235+
case 'boolean':
236+
return 'bool';
237+
case 'double':
238+
case 'real':
239+
case 'float':
240+
return 'float';
241+
case 'int':
242+
case 'integer':
243+
return 'int';
244+
case 'array()':
245+
case 'array':
246+
return 'array';
247+
}
248+
249+
if (strpos($lowerVarType, 'array(') !== false) {
250+
// Valid array declaration:
251+
// array, array(type), array(type1 => type2).
252+
$matches = [];
253+
$pattern = '/^array\(\s*([^\s^=^>]*)(\s*=>\s*(.*))?\s*\)/i';
254+
if (preg_match($pattern, $varType, $matches) !== 0) {
255+
$type1 = '';
256+
if (isset($matches[1]) === true) {
257+
$type1 = $matches[1];
258+
}
259+
260+
$type2 = '';
261+
if (isset($matches[3]) === true) {
262+
$type2 = $matches[3];
263+
}
264+
265+
$type1 = self::suggestType($type1);
266+
$type2 = self::suggestType($type2);
267+
268+
// Note: The phpdoc array syntax only allows you to describe the array value type.
269+
// https://docs.phpdoc.org/latest/guide/guides/types.html#arrays
270+
if ($type1 && !$type2) {
271+
// This is an array of [type2, type2, type2].
272+
return "{$type1}[]";
273+
}
274+
// This is an array of [type1 => type2, type1 => type2, type1 => type2].
275+
return "{$type2}[]";
276+
} else {
277+
return 'array';
278+
}
279+
} elseif (in_array($lowerVarType, self::$allowedTypes, true) === true) {
280+
// A valid type, but not lower cased.
281+
return $lowerVarType;
282+
} else {
283+
// Must be a custom type name.
284+
return $varType;
285+
}
286+
}
287+
}
288+
289+
/**
290+
* @codeCoverageIgnore
291+
*/
292+
protected function processVariableInString(File $phpcsFile, $stackPtr) {
293+
}
294+
}

0 commit comments

Comments
 (0)