Chris@0
|
1 <?php
|
Chris@0
|
2 /**
|
Chris@0
|
3 * Parses and verifies the doc comments for functions.
|
Chris@0
|
4 *
|
Chris@0
|
5 * @category PHP
|
Chris@0
|
6 * @package PHP_CodeSniffer
|
Chris@0
|
7 * @link http://pear.php.net/package/PHP_CodeSniffer
|
Chris@0
|
8 */
|
Chris@0
|
9
|
Chris@17
|
10 namespace Drupal\Sniffs\Commenting;
|
Chris@17
|
11
|
Chris@17
|
12 use PHP_CodeSniffer\Files\File;
|
Chris@17
|
13 use PHP_CodeSniffer\Sniffs\Sniff;
|
Chris@17
|
14 use PHP_CodeSniffer\Util\Tokens;
|
Chris@17
|
15
|
Chris@0
|
16 /**
|
Chris@0
|
17 * Parses and verifies the doc comments for functions. Largely copied from
|
Chris@17
|
18 * PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\FunctionCommentSniff.
|
Chris@0
|
19 *
|
Chris@0
|
20 * @category PHP
|
Chris@0
|
21 * @package PHP_CodeSniffer
|
Chris@0
|
22 * @link http://pear.php.net/package/PHP_CodeSniffer
|
Chris@0
|
23 */
|
Chris@17
|
24 class FunctionCommentSniff implements Sniff
|
Chris@0
|
25 {
|
Chris@0
|
26
|
Chris@0
|
27 /**
|
Chris@0
|
28 * A map of invalid data types to valid ones for param and return documentation.
|
Chris@0
|
29 *
|
Chris@0
|
30 * @var array
|
Chris@0
|
31 */
|
Chris@0
|
32 public static $invalidTypes = array(
|
Chris@0
|
33 'Array' => 'array',
|
Chris@0
|
34 'array()' => 'array',
|
Chris@0
|
35 '[]' => 'array',
|
Chris@0
|
36 'boolean' => 'bool',
|
Chris@0
|
37 'Boolean' => 'bool',
|
Chris@0
|
38 'integer' => 'int',
|
Chris@0
|
39 'str' => 'string',
|
Chris@0
|
40 'stdClass' => 'object',
|
Chris@0
|
41 'number' => 'int',
|
Chris@0
|
42 'String' => 'string',
|
Chris@0
|
43 'type' => 'mixed',
|
Chris@0
|
44 'NULL' => 'null',
|
Chris@0
|
45 'FALSE' => 'false',
|
Chris@0
|
46 'TRUE' => 'true',
|
Chris@0
|
47 'Bool' => 'bool',
|
Chris@0
|
48 'Int' => 'int',
|
Chris@0
|
49 'Integer' => 'int',
|
Chris@0
|
50 );
|
Chris@0
|
51
|
Chris@0
|
52 /**
|
Chris@0
|
53 * An array of variable types for param/var we will check.
|
Chris@0
|
54 *
|
Chris@0
|
55 * @var array(string)
|
Chris@0
|
56 */
|
Chris@0
|
57 public $allowedTypes = array(
|
Chris@0
|
58 'array',
|
Chris@0
|
59 'mixed',
|
Chris@0
|
60 'object',
|
Chris@0
|
61 'resource',
|
Chris@0
|
62 'callable',
|
Chris@0
|
63 );
|
Chris@0
|
64
|
Chris@0
|
65
|
Chris@0
|
66 /**
|
Chris@0
|
67 * Returns an array of tokens this test wants to listen for.
|
Chris@0
|
68 *
|
Chris@0
|
69 * @return array
|
Chris@0
|
70 */
|
Chris@0
|
71 public function register()
|
Chris@0
|
72 {
|
Chris@0
|
73 return array(T_FUNCTION);
|
Chris@0
|
74
|
Chris@0
|
75 }//end register()
|
Chris@0
|
76
|
Chris@0
|
77
|
Chris@0
|
78 /**
|
Chris@0
|
79 * Processes this test, when one of its tokens is encountered.
|
Chris@0
|
80 *
|
Chris@17
|
81 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
Chris@17
|
82 * @param int $stackPtr The position of the current token
|
Chris@17
|
83 * in the stack passed in $tokens.
|
Chris@0
|
84 *
|
Chris@0
|
85 * @return void
|
Chris@0
|
86 */
|
Chris@17
|
87 public function process(File $phpcsFile, $stackPtr)
|
Chris@0
|
88 {
|
Chris@0
|
89 $tokens = $phpcsFile->getTokens();
|
Chris@17
|
90 $find = Tokens::$methodPrefixes;
|
Chris@0
|
91 $find[] = T_WHITESPACE;
|
Chris@0
|
92
|
Chris@0
|
93 $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
|
Chris@17
|
94 $beforeCommentEnd = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($commentEnd - 1), null, true);
|
Chris@0
|
95 if (($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
|
Chris@0
|
96 && $tokens[$commentEnd]['code'] !== T_COMMENT)
|
Chris@0
|
97 || ($beforeCommentEnd !== false
|
Chris@0
|
98 // If there is something more on the line than just the comment then the
|
Chris@0
|
99 // comment does not belong to the function.
|
Chris@0
|
100 && $tokens[$beforeCommentEnd]['line'] === $tokens[$commentEnd]['line'])
|
Chris@0
|
101 ) {
|
Chris@0
|
102 $fix = $phpcsFile->addFixableError('Missing function doc comment', $stackPtr, 'Missing');
|
Chris@0
|
103 if ($fix === true) {
|
Chris@0
|
104 $before = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), ($stackPtr + 1), true);
|
Chris@0
|
105 $phpcsFile->fixer->addContentBefore($before, "/**\n *\n */\n");
|
Chris@0
|
106 }
|
Chris@0
|
107
|
Chris@0
|
108 return;
|
Chris@0
|
109 }
|
Chris@0
|
110
|
Chris@0
|
111 if ($tokens[$commentEnd]['code'] === T_COMMENT) {
|
Chris@0
|
112 $fix = $phpcsFile->addFixableError('You must use "/**" style comments for a function comment', $stackPtr, 'WrongStyle');
|
Chris@0
|
113 if ($fix === true) {
|
Chris@0
|
114 // Convert the comment into a doc comment.
|
Chris@0
|
115 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
116 $comment = '';
|
Chris@0
|
117 for ($i = $commentEnd; $tokens[$i]['code'] === T_COMMENT; $i--) {
|
Chris@0
|
118 $comment = ' *'.ltrim($tokens[$i]['content'], '/* ').$comment;
|
Chris@0
|
119 $phpcsFile->fixer->replaceToken($i, '');
|
Chris@0
|
120 }
|
Chris@0
|
121
|
Chris@0
|
122 $phpcsFile->fixer->replaceToken($commentEnd, "/**\n".rtrim($comment, "*/\n")."\n */\n");
|
Chris@0
|
123 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
124 }
|
Chris@0
|
125
|
Chris@0
|
126 return;
|
Chris@0
|
127 }
|
Chris@0
|
128
|
Chris@0
|
129 $commentStart = $tokens[$commentEnd]['comment_opener'];
|
Chris@0
|
130 foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
|
Chris@0
|
131 // This is a file comment, not a function comment.
|
Chris@0
|
132 if ($tokens[$tag]['content'] === '@file') {
|
Chris@0
|
133 $fix = $phpcsFile->addFixableError('Missing function doc comment', $stackPtr, 'Missing');
|
Chris@0
|
134 if ($fix === true) {
|
Chris@0
|
135 $before = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), ($stackPtr + 1), true);
|
Chris@0
|
136 $phpcsFile->fixer->addContentBefore($before, "/**\n *\n */\n");
|
Chris@0
|
137 }
|
Chris@0
|
138
|
Chris@0
|
139 return;
|
Chris@0
|
140 }
|
Chris@0
|
141
|
Chris@0
|
142 if ($tokens[$tag]['content'] === '@see') {
|
Chris@0
|
143 // Make sure the tag isn't empty.
|
Chris@0
|
144 $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
|
Chris@0
|
145 if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
|
Chris@0
|
146 $error = 'Content missing for @see tag in function comment';
|
Chris@0
|
147 $phpcsFile->addError($error, $tag, 'EmptySees');
|
Chris@0
|
148 }
|
Chris@0
|
149 }
|
Chris@0
|
150 }//end foreach
|
Chris@0
|
151
|
Chris@0
|
152 if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) {
|
Chris@0
|
153 $error = 'There must be no blank lines after the function comment';
|
Chris@0
|
154 $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter');
|
Chris@0
|
155 if ($fix === true) {
|
Chris@0
|
156 $phpcsFile->fixer->replaceToken(($commentEnd + 1), '');
|
Chris@0
|
157 }
|
Chris@0
|
158 }
|
Chris@0
|
159
|
Chris@0
|
160 $this->processReturn($phpcsFile, $stackPtr, $commentStart);
|
Chris@0
|
161 $this->processThrows($phpcsFile, $stackPtr, $commentStart);
|
Chris@0
|
162 $this->processParams($phpcsFile, $stackPtr, $commentStart);
|
Chris@0
|
163 $this->processSees($phpcsFile, $stackPtr, $commentStart);
|
Chris@0
|
164
|
Chris@0
|
165 }//end process()
|
Chris@0
|
166
|
Chris@0
|
167
|
Chris@0
|
168 /**
|
Chris@0
|
169 * Process the return comment of this function comment.
|
Chris@0
|
170 *
|
Chris@17
|
171 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
Chris@17
|
172 * @param int $stackPtr The position of the current token
|
Chris@17
|
173 * in the stack passed in $tokens.
|
Chris@17
|
174 * @param int $commentStart The position in the stack where the comment started.
|
Chris@0
|
175 *
|
Chris@0
|
176 * @return void
|
Chris@0
|
177 */
|
Chris@17
|
178 protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
|
Chris@0
|
179 {
|
Chris@0
|
180 $tokens = $phpcsFile->getTokens();
|
Chris@0
|
181
|
Chris@0
|
182 // Skip constructor and destructor.
|
Chris@0
|
183 $className = '';
|
Chris@0
|
184 foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) {
|
Chris@0
|
185 if ($condition === T_CLASS || $condition === T_INTERFACE) {
|
Chris@0
|
186 $className = $phpcsFile->getDeclarationName($condPtr);
|
Chris@0
|
187 $className = strtolower(ltrim($className, '_'));
|
Chris@0
|
188 }
|
Chris@0
|
189 }
|
Chris@0
|
190
|
Chris@0
|
191 $methodName = $phpcsFile->getDeclarationName($stackPtr);
|
Chris@0
|
192 $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct');
|
Chris@0
|
193 $methodName = strtolower(ltrim($methodName, '_'));
|
Chris@0
|
194
|
Chris@0
|
195 $return = null;
|
Chris@0
|
196 foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
|
Chris@0
|
197 if ($tokens[$tag]['content'] === '@return') {
|
Chris@0
|
198 if ($return !== null) {
|
Chris@0
|
199 $error = 'Only 1 @return tag is allowed in a function comment';
|
Chris@0
|
200 $phpcsFile->addError($error, $tag, 'DuplicateReturn');
|
Chris@0
|
201 return;
|
Chris@0
|
202 }
|
Chris@0
|
203
|
Chris@0
|
204 $return = $tag;
|
Chris@0
|
205 // Any strings until the next tag belong to this comment.
|
Chris@0
|
206 if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
|
Chris@0
|
207 $end = $tokens[$commentStart]['comment_tags'][($pos + 1)];
|
Chris@0
|
208 } else {
|
Chris@0
|
209 $end = $tokens[$commentStart]['comment_closer'];
|
Chris@0
|
210 }
|
Chris@0
|
211 }
|
Chris@0
|
212 }
|
Chris@0
|
213
|
Chris@0
|
214 $type = null;
|
Chris@0
|
215 if ($isSpecialMethod === false && $methodName !== $className) {
|
Chris@0
|
216 if ($return !== null) {
|
Chris@0
|
217 $type = trim($tokens[($return + 2)]['content']);
|
Chris@0
|
218 if (empty($type) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) {
|
Chris@0
|
219 $error = 'Return type missing for @return tag in function comment';
|
Chris@0
|
220 $phpcsFile->addError($error, $return, 'MissingReturnType');
|
Chris@0
|
221 } else if (strpos($type, ' ') === false) {
|
Chris@0
|
222 // Check return type (can be multiple, separated by '|').
|
Chris@0
|
223 $typeNames = explode('|', $type);
|
Chris@0
|
224 $suggestedNames = array();
|
Chris@0
|
225 $hasNull = false;
|
Chris@0
|
226 $hasMultiple = false;
|
Chris@0
|
227 if (count($typeNames) > 0) {
|
Chris@0
|
228 $hasMultiple = true;
|
Chris@0
|
229 }
|
Chris@0
|
230
|
Chris@0
|
231 foreach ($typeNames as $i => $typeName) {
|
Chris@0
|
232 if (strtolower($typeName) === 'null') {
|
Chris@0
|
233 $hasNull = true;
|
Chris@0
|
234 }
|
Chris@0
|
235
|
Chris@0
|
236 $suggestedName = static::suggestType($typeName);
|
Chris@0
|
237 if (in_array($suggestedName, $suggestedNames) === false) {
|
Chris@0
|
238 $suggestedNames[] = $suggestedName;
|
Chris@0
|
239 }
|
Chris@0
|
240 }
|
Chris@0
|
241
|
Chris@0
|
242 $suggestedType = implode('|', $suggestedNames);
|
Chris@0
|
243 if ($type !== $suggestedType) {
|
Chris@0
|
244 $error = 'Expected "%s" but found "%s" for function return type';
|
Chris@0
|
245 $data = array(
|
Chris@0
|
246 $suggestedType,
|
Chris@0
|
247 $type,
|
Chris@0
|
248 );
|
Chris@0
|
249 $fix = $phpcsFile->addFixableError($error, $return, 'InvalidReturn', $data);
|
Chris@0
|
250 if ($fix === true) {
|
Chris@0
|
251 $content = $suggestedType;
|
Chris@0
|
252 $phpcsFile->fixer->replaceToken(($return + 2), $content);
|
Chris@0
|
253 }
|
Chris@0
|
254 }//end if
|
Chris@0
|
255
|
Chris@0
|
256 if ($type === 'void') {
|
Chris@0
|
257 $error = 'If there is no return value for a function, there must not be a @return tag.';
|
Chris@0
|
258 $phpcsFile->addError($error, $return, 'VoidReturn');
|
Chris@0
|
259 } else if ($type !== 'mixed') {
|
Chris@0
|
260 // If return type is not void, there needs to be a return statement
|
Chris@0
|
261 // somewhere in the function that returns something.
|
Chris@0
|
262 if (isset($tokens[$stackPtr]['scope_closer']) === true) {
|
Chris@0
|
263 $endToken = $tokens[$stackPtr]['scope_closer'];
|
Chris@0
|
264 $foundReturnToken = false;
|
Chris@0
|
265 $searchStart = $stackPtr;
|
Chris@0
|
266 $foundNonVoidReturn = false;
|
Chris@0
|
267 do {
|
Chris@18
|
268 $returnToken = $phpcsFile->findNext(array(T_RETURN, T_YIELD), $searchStart, $endToken);
|
Chris@0
|
269 if ($returnToken === false && $foundReturnToken === false) {
|
Chris@0
|
270 $error = '@return doc comment specified, but function has no return statement';
|
Chris@0
|
271 $phpcsFile->addError($error, $return, 'InvalidNoReturn');
|
Chris@0
|
272 } else {
|
Chris@0
|
273 // Check for return token as the last loop after the last return
|
Chris@0
|
274 // in the function will enter this else condition
|
Chris@0
|
275 // but without the returnToken.
|
Chris@0
|
276 if ($returnToken !== false) {
|
Chris@0
|
277 $foundReturnToken = true;
|
Chris@0
|
278 $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
|
Chris@0
|
279 if ($tokens[$semicolon]['code'] === T_SEMICOLON) {
|
Chris@0
|
280 // Void return is allowed if the @return type has null in it.
|
Chris@0
|
281 if ($hasNull === false) {
|
Chris@0
|
282 $error = 'Function return type is not void, but function is returning void here';
|
Chris@0
|
283 $phpcsFile->addError($error, $returnToken, 'InvalidReturnNotVoid');
|
Chris@0
|
284 }
|
Chris@0
|
285 } else {
|
Chris@0
|
286 $foundNonVoidReturn = true;
|
Chris@0
|
287 }//end if
|
Chris@0
|
288
|
Chris@0
|
289 $searchStart = ($returnToken + 1);
|
Chris@0
|
290 }//end if
|
Chris@0
|
291 }//end if
|
Chris@0
|
292 } while ($returnToken !== false);
|
Chris@0
|
293
|
Chris@0
|
294 if ($foundNonVoidReturn === false && $foundReturnToken === true) {
|
Chris@0
|
295 $error = 'Function return type is not void, but function does not have a non-void return statement';
|
Chris@0
|
296 $phpcsFile->addError($error, $return, 'InvalidReturnNotVoid');
|
Chris@0
|
297 }
|
Chris@0
|
298 }//end if
|
Chris@0
|
299 }//end if
|
Chris@0
|
300 }//end if
|
Chris@0
|
301
|
Chris@0
|
302 $comment = '';
|
Chris@0
|
303 for ($i = ($return + 3); $i < $end; $i++) {
|
Chris@0
|
304 if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
|
Chris@0
|
305 $indent = 0;
|
Chris@0
|
306 if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
|
Chris@0
|
307 $indent = strlen($tokens[($i - 1)]['content']);
|
Chris@0
|
308 }
|
Chris@0
|
309
|
Chris@0
|
310 $comment .= ' '.$tokens[$i]['content'];
|
Chris@0
|
311 $commentLines[] = array(
|
Chris@0
|
312 'comment' => $tokens[$i]['content'],
|
Chris@0
|
313 'token' => $i,
|
Chris@0
|
314 'indent' => $indent,
|
Chris@0
|
315 );
|
Chris@0
|
316 if ($indent < 3) {
|
Chris@0
|
317 $error = 'Return comment indentation must be 3 spaces, found %s spaces';
|
Chris@0
|
318 $fix = $phpcsFile->addFixableError($error, $i, 'ReturnCommentIndentation', array($indent));
|
Chris@0
|
319 if ($fix === true) {
|
Chris@0
|
320 $phpcsFile->fixer->replaceToken(($i - 1), ' ');
|
Chris@0
|
321 }
|
Chris@0
|
322 }
|
Chris@0
|
323 }
|
Chris@0
|
324 }//end for
|
Chris@0
|
325
|
Chris@0
|
326 // The first line of the comment must be indented no more than 3
|
Chris@0
|
327 // spaces, the following lines can be more so we only check the first
|
Chris@0
|
328 // line.
|
Chris@0
|
329 if (empty($commentLines[0]['indent']) === false && $commentLines[0]['indent'] > 3) {
|
Chris@0
|
330 $error = 'Return comment indentation must be 3 spaces, found %s spaces';
|
Chris@0
|
331 $fix = $phpcsFile->addFixableError($error, ($commentLines[0]['token'] - 1), 'ReturnCommentIndentation', array($commentLines[0]['indent']));
|
Chris@0
|
332 if ($fix === true) {
|
Chris@0
|
333 $phpcsFile->fixer->replaceToken(($commentLines[0]['token'] - 1), ' ');
|
Chris@0
|
334 }
|
Chris@0
|
335 }
|
Chris@0
|
336
|
Chris@0
|
337 if ($comment === '' && $type !== '$this' && $type !== 'static') {
|
Chris@0
|
338 if (strpos($type, ' ') !== false) {
|
Chris@0
|
339 $error = 'Description for the @return value must be on the next line';
|
Chris@0
|
340 } else {
|
Chris@0
|
341 $error = 'Description for the @return value is missing';
|
Chris@0
|
342 }
|
Chris@0
|
343
|
Chris@0
|
344 $phpcsFile->addError($error, $return, 'MissingReturnComment');
|
Chris@0
|
345 } else if (strpos($type, ' ') !== false) {
|
Chris@0
|
346 if (preg_match('/^([^\s]+)[\s]+(\$[^\s]+)[\s]*$/', $type, $matches) === 1) {
|
Chris@0
|
347 $error = 'Return type must not contain variable name "%s"';
|
Chris@0
|
348 $data = array($matches[2]);
|
Chris@0
|
349 $fix = $phpcsFile->addFixableError($error, ($return + 2), 'ReturnVarName', $data);
|
Chris@0
|
350 if ($fix === true) {
|
Chris@0
|
351 $phpcsFile->fixer->replaceToken(($return + 2), $matches[1]);
|
Chris@0
|
352 }
|
Chris@0
|
353 } else {
|
Chris@0
|
354 $error = 'Return type "%s" must not contain spaces';
|
Chris@0
|
355 $data = array($type);
|
Chris@0
|
356 $phpcsFile->addError($error, $return, 'ReturnTypeSpaces', $data);
|
Chris@0
|
357 }
|
Chris@0
|
358 }//end if
|
Chris@0
|
359 }//end if
|
Chris@0
|
360 } else {
|
Chris@0
|
361 // No return tag for constructor and destructor.
|
Chris@0
|
362 if ($return !== null) {
|
Chris@0
|
363 $error = '@return tag is not required for constructor and destructor';
|
Chris@0
|
364 $phpcsFile->addError($error, $return, 'ReturnNotRequired');
|
Chris@0
|
365 }
|
Chris@0
|
366 }//end if
|
Chris@0
|
367
|
Chris@0
|
368 }//end processReturn()
|
Chris@0
|
369
|
Chris@0
|
370
|
Chris@0
|
371 /**
|
Chris@0
|
372 * Process any throw tags that this function comment has.
|
Chris@0
|
373 *
|
Chris@17
|
374 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
Chris@17
|
375 * @param int $stackPtr The position of the current token
|
Chris@17
|
376 * in the stack passed in $tokens.
|
Chris@17
|
377 * @param int $commentStart The position in the stack where the comment started.
|
Chris@0
|
378 *
|
Chris@0
|
379 * @return void
|
Chris@0
|
380 */
|
Chris@17
|
381 protected function processThrows(File $phpcsFile, $stackPtr, $commentStart)
|
Chris@0
|
382 {
|
Chris@0
|
383 $tokens = $phpcsFile->getTokens();
|
Chris@0
|
384
|
Chris@0
|
385 foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
|
Chris@0
|
386 if ($tokens[$tag]['content'] !== '@throws') {
|
Chris@0
|
387 continue;
|
Chris@0
|
388 }
|
Chris@0
|
389
|
Chris@0
|
390 if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
|
Chris@0
|
391 $error = 'Exception type missing for @throws tag in function comment';
|
Chris@0
|
392 $phpcsFile->addError($error, $tag, 'InvalidThrows');
|
Chris@0
|
393 } else {
|
Chris@0
|
394 // Any strings until the next tag belong to this comment.
|
Chris@0
|
395 if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
|
Chris@0
|
396 $end = $tokens[$commentStart]['comment_tags'][($pos + 1)];
|
Chris@0
|
397 } else {
|
Chris@0
|
398 $end = $tokens[$commentStart]['comment_closer'];
|
Chris@0
|
399 }
|
Chris@0
|
400
|
Chris@0
|
401 $comment = '';
|
Chris@0
|
402 $throwStart = null;
|
Chris@0
|
403 for ($i = ($tag + 3); $i < $end; $i++) {
|
Chris@0
|
404 if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
|
Chris@0
|
405 if ($throwStart === null) {
|
Chris@0
|
406 $throwStart = $i;
|
Chris@0
|
407 }
|
Chris@0
|
408
|
Chris@0
|
409 $indent = 0;
|
Chris@0
|
410 if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
|
Chris@0
|
411 $indent = strlen($tokens[($i - 1)]['content']);
|
Chris@0
|
412 }
|
Chris@0
|
413
|
Chris@0
|
414 $comment .= ' '.$tokens[$i]['content'];
|
Chris@0
|
415 if ($indent < 3) {
|
Chris@0
|
416 $error = 'Throws comment indentation must be 3 spaces, found %s spaces';
|
Chris@0
|
417 $phpcsFile->addError($error, $i, 'TrhowsCommentIndentation', array($indent));
|
Chris@0
|
418 }
|
Chris@0
|
419 }
|
Chris@0
|
420 }
|
Chris@0
|
421
|
Chris@0
|
422 $comment = trim($comment);
|
Chris@0
|
423
|
Chris@0
|
424 if ($comment === '') {
|
Chris@0
|
425 if (str_word_count($tokens[($tag + 2)]['content'], 0, '\\_') > 1) {
|
Chris@0
|
426 $error = '@throws comment must be on the next line';
|
Chris@0
|
427 $phpcsFile->addError($error, $tag, 'ThrowsComment');
|
Chris@0
|
428 }
|
Chris@0
|
429
|
Chris@0
|
430 return;
|
Chris@0
|
431 }
|
Chris@0
|
432
|
Chris@0
|
433 // Starts with a capital letter and ends with a fullstop.
|
Chris@0
|
434 $firstChar = $comment{0};
|
Chris@0
|
435 if (strtoupper($firstChar) !== $firstChar) {
|
Chris@0
|
436 $error = '@throws tag comment must start with a capital letter';
|
Chris@0
|
437 $phpcsFile->addError($error, $throwStart, 'ThrowsNotCapital');
|
Chris@0
|
438 }
|
Chris@0
|
439
|
Chris@0
|
440 $lastChar = substr($comment, -1);
|
Chris@0
|
441 if (in_array($lastChar, array('.', '!', '?')) === false) {
|
Chris@0
|
442 $error = '@throws tag comment must end with a full stop';
|
Chris@0
|
443 $phpcsFile->addError($error, $throwStart, 'ThrowsNoFullStop');
|
Chris@0
|
444 }
|
Chris@0
|
445 }//end if
|
Chris@0
|
446 }//end foreach
|
Chris@0
|
447
|
Chris@0
|
448 }//end processThrows()
|
Chris@0
|
449
|
Chris@0
|
450
|
Chris@0
|
451 /**
|
Chris@0
|
452 * Process the function parameter comments.
|
Chris@0
|
453 *
|
Chris@17
|
454 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
Chris@17
|
455 * @param int $stackPtr The position of the current token
|
Chris@17
|
456 * in the stack passed in $tokens.
|
Chris@17
|
457 * @param int $commentStart The position in the stack where the comment started.
|
Chris@0
|
458 *
|
Chris@0
|
459 * @return void
|
Chris@0
|
460 */
|
Chris@17
|
461 protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
|
Chris@0
|
462 {
|
Chris@0
|
463 $tokens = $phpcsFile->getTokens();
|
Chris@0
|
464
|
Chris@0
|
465 $params = array();
|
Chris@0
|
466 $maxType = 0;
|
Chris@0
|
467 $maxVar = 0;
|
Chris@0
|
468 foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
|
Chris@0
|
469 if ($tokens[$tag]['content'] !== '@param') {
|
Chris@0
|
470 continue;
|
Chris@0
|
471 }
|
Chris@0
|
472
|
Chris@0
|
473 $type = '';
|
Chris@0
|
474 $typeSpace = 0;
|
Chris@0
|
475 $var = '';
|
Chris@0
|
476 $varSpace = 0;
|
Chris@0
|
477 $comment = '';
|
Chris@0
|
478 $commentLines = array();
|
Chris@0
|
479 if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
|
Chris@0
|
480 $matches = array();
|
Chris@0
|
481 preg_match('/([^$&]*)(?:((?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches);
|
Chris@0
|
482
|
Chris@0
|
483 $typeLen = strlen($matches[1]);
|
Chris@0
|
484 $type = trim($matches[1]);
|
Chris@0
|
485 $typeSpace = ($typeLen - strlen($type));
|
Chris@0
|
486 $typeLen = strlen($type);
|
Chris@0
|
487 if ($typeLen > $maxType) {
|
Chris@0
|
488 $maxType = $typeLen;
|
Chris@0
|
489 }
|
Chris@0
|
490
|
Chris@0
|
491 // If there is more than one word then it is a comment that should be
|
Chris@0
|
492 // on the next line.
|
Chris@0
|
493 if (isset($matches[4]) === true && ($typeLen > 0
|
Chris@0
|
494 || preg_match('/[^\s]+[\s]+[^\s]+/', $matches[4]) === 1)
|
Chris@0
|
495 ) {
|
Chris@0
|
496 $comment = $matches[4];
|
Chris@0
|
497 $error = 'Parameter comment must be on the next line';
|
Chris@0
|
498 $fix = $phpcsFile->addFixableError($error, ($tag + 2), 'ParamCommentNewLine');
|
Chris@0
|
499 if ($fix === true) {
|
Chris@0
|
500 $parts = $matches;
|
Chris@0
|
501 unset($parts[0]);
|
Chris@0
|
502 $parts[3] = "\n * ";
|
Chris@0
|
503 $phpcsFile->fixer->replaceToken(($tag + 2), implode('', $parts));
|
Chris@0
|
504 }
|
Chris@0
|
505 }
|
Chris@0
|
506
|
Chris@0
|
507 if (isset($matches[2]) === true) {
|
Chris@0
|
508 $var = $matches[2];
|
Chris@0
|
509 } else {
|
Chris@0
|
510 $var = '';
|
Chris@0
|
511 }
|
Chris@0
|
512
|
Chris@0
|
513 if (substr($var, -1) === '.') {
|
Chris@0
|
514 $error = 'Doc comment parameter name "%s" must not end with a dot';
|
Chris@0
|
515 $fix = $phpcsFile->addFixableError($error, ($tag + 2), 'ParamNameDot', [$var]);
|
Chris@0
|
516 if ($fix === true) {
|
Chris@0
|
517 $content = $type.' '.substr($var, 0, -1);
|
Chris@0
|
518 $phpcsFile->fixer->replaceToken(($tag + 2), $content);
|
Chris@0
|
519 }
|
Chris@0
|
520
|
Chris@0
|
521 // Continue with the next parameter to avoid confusing
|
Chris@0
|
522 // overlapping errors further down.
|
Chris@0
|
523 continue;
|
Chris@0
|
524 }
|
Chris@0
|
525
|
Chris@0
|
526 $varLen = strlen($var);
|
Chris@0
|
527 if ($varLen > $maxVar) {
|
Chris@0
|
528 $maxVar = $varLen;
|
Chris@0
|
529 }
|
Chris@0
|
530
|
Chris@0
|
531 // Any strings until the next tag belong to this comment.
|
Chris@0
|
532 if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
|
Chris@0
|
533 $end = $tokens[$commentStart]['comment_tags'][($pos + 1)];
|
Chris@0
|
534 } else {
|
Chris@0
|
535 $end = $tokens[$commentStart]['comment_closer'];
|
Chris@0
|
536 }
|
Chris@0
|
537
|
Chris@0
|
538 for ($i = ($tag + 3); $i < $end; $i++) {
|
Chris@0
|
539 if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
|
Chris@0
|
540 $indent = 0;
|
Chris@0
|
541 if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
|
Chris@0
|
542 $indent = strlen($tokens[($i - 1)]['content']);
|
Chris@0
|
543 }
|
Chris@0
|
544
|
Chris@0
|
545 $comment .= ' '.$tokens[$i]['content'];
|
Chris@0
|
546 $commentLines[] = array(
|
Chris@0
|
547 'comment' => $tokens[$i]['content'],
|
Chris@0
|
548 'token' => $i,
|
Chris@0
|
549 'indent' => $indent,
|
Chris@0
|
550 );
|
Chris@0
|
551 if ($indent < 3) {
|
Chris@0
|
552 $error = 'Parameter comment indentation must be 3 spaces, found %s spaces';
|
Chris@0
|
553 $fix = $phpcsFile->addFixableError($error, $i, 'ParamCommentIndentation', array($indent));
|
Chris@0
|
554 if ($fix === true) {
|
Chris@0
|
555 $phpcsFile->fixer->replaceToken(($i - 1), ' ');
|
Chris@0
|
556 }
|
Chris@0
|
557 }
|
Chris@0
|
558 }
|
Chris@0
|
559 }//end for
|
Chris@0
|
560
|
Chris@0
|
561 // The first line of the comment must be indented no more than 3
|
Chris@0
|
562 // spaces, the following lines can be more so we only check the first
|
Chris@0
|
563 // line.
|
Chris@0
|
564 if (empty($commentLines[0]['indent']) === false && $commentLines[0]['indent'] > 3) {
|
Chris@0
|
565 $error = 'Parameter comment indentation must be 3 spaces, found %s spaces';
|
Chris@0
|
566 $fix = $phpcsFile->addFixableError($error, ($commentLines[0]['token'] - 1), 'ParamCommentIndentation', array($commentLines[0]['indent']));
|
Chris@0
|
567 if ($fix === true) {
|
Chris@0
|
568 $phpcsFile->fixer->replaceToken(($commentLines[0]['token'] - 1), ' ');
|
Chris@0
|
569 }
|
Chris@0
|
570 }
|
Chris@0
|
571
|
Chris@0
|
572 if ($comment === '') {
|
Chris@0
|
573 $error = 'Missing parameter comment';
|
Chris@0
|
574 $phpcsFile->addError($error, $tag, 'MissingParamComment');
|
Chris@0
|
575 $commentLines[] = array('comment' => '');
|
Chris@0
|
576 }//end if
|
Chris@17
|
577
|
Chris@0
|
578 $variableArguments = false;
|
Chris@0
|
579 // Allow the "..." @param doc for a variable number of parameters.
|
Chris@0
|
580 // This could happen with type defined as @param array ... or
|
Chris@0
|
581 // without type defined as @param ...
|
Chris@0
|
582 if ($tokens[($tag + 2)]['content'] === '...'
|
Chris@0
|
583 || (substr($tokens[($tag + 2)]['content'], -3) === '...'
|
Chris@0
|
584 && count(explode(' ', $tokens[($tag + 2)]['content'])) === 2)
|
Chris@0
|
585 ) {
|
Chris@0
|
586 $variableArguments = true;
|
Chris@0
|
587 }
|
Chris@0
|
588
|
Chris@0
|
589 if ($typeLen === 0) {
|
Chris@0
|
590 $error = 'Missing parameter type';
|
Chris@0
|
591 // If there is just one word as comment at the end of the line
|
Chris@0
|
592 // then this is probably the data type. Move it before the
|
Chris@0
|
593 // variable name.
|
Chris@0
|
594 if (isset($matches[4]) === true && preg_match('/[^\s]+[\s]+[^\s]+/', $matches[4]) === 0) {
|
Chris@0
|
595 $fix = $phpcsFile->addFixableError($error, $tag, 'MissingParamType');
|
Chris@0
|
596 if ($fix === true) {
|
Chris@0
|
597 $phpcsFile->fixer->replaceToken(($tag + 2), $matches[4].' '.$var);
|
Chris@0
|
598 }
|
Chris@0
|
599 } else {
|
Chris@0
|
600 $phpcsFile->addError($error, $tag, 'MissingParamType');
|
Chris@0
|
601 }
|
Chris@0
|
602 }
|
Chris@0
|
603
|
Chris@0
|
604 if (empty($matches[2]) === true && $variableArguments === false) {
|
Chris@0
|
605 $error = 'Missing parameter name';
|
Chris@0
|
606 $phpcsFile->addError($error, $tag, 'MissingParamName');
|
Chris@0
|
607 }
|
Chris@0
|
608 } else {
|
Chris@0
|
609 $error = 'Missing parameter type';
|
Chris@0
|
610 $phpcsFile->addError($error, $tag, 'MissingParamType');
|
Chris@0
|
611 }//end if
|
Chris@0
|
612
|
Chris@0
|
613 $params[] = array(
|
Chris@0
|
614 'tag' => $tag,
|
Chris@0
|
615 'type' => $type,
|
Chris@0
|
616 'var' => $var,
|
Chris@0
|
617 'comment' => $comment,
|
Chris@0
|
618 'commentLines' => $commentLines,
|
Chris@0
|
619 'type_space' => $typeSpace,
|
Chris@0
|
620 'var_space' => $varSpace,
|
Chris@0
|
621 );
|
Chris@0
|
622 }//end foreach
|
Chris@0
|
623
|
Chris@0
|
624 $realParams = $phpcsFile->getMethodParameters($stackPtr);
|
Chris@0
|
625 $foundParams = array();
|
Chris@0
|
626
|
Chris@0
|
627 $checkPos = 0;
|
Chris@0
|
628 foreach ($params as $pos => $param) {
|
Chris@0
|
629 if ($param['var'] === '') {
|
Chris@0
|
630 continue;
|
Chris@0
|
631 }
|
Chris@0
|
632
|
Chris@0
|
633 $foundParams[] = $param['var'];
|
Chris@0
|
634
|
Chris@0
|
635 // If the type is empty, the whole line is empty.
|
Chris@0
|
636 if ($param['type'] === '') {
|
Chris@0
|
637 continue;
|
Chris@0
|
638 }
|
Chris@0
|
639
|
Chris@0
|
640 // Make sure the param name is correct.
|
Chris@0
|
641 $matched = false;
|
Chris@0
|
642 // Parameter documentation can be omitted for some parameters, so we have
|
Chris@0
|
643 // to search the rest for a match.
|
Chris@0
|
644 $realName = '<undefined>';
|
Chris@0
|
645 while (isset($realParams[($checkPos)]) === true) {
|
Chris@0
|
646 $realName = $realParams[$checkPos]['name'];
|
Chris@0
|
647
|
Chris@0
|
648 if ($realName === $param['var'] || ($realParams[$checkPos]['pass_by_reference'] === true
|
Chris@0
|
649 && ('&'.$realName) === $param['var'])
|
Chris@0
|
650 ) {
|
Chris@0
|
651 $matched = true;
|
Chris@0
|
652 break;
|
Chris@0
|
653 }
|
Chris@0
|
654
|
Chris@0
|
655 $checkPos++;
|
Chris@0
|
656 }
|
Chris@0
|
657
|
Chris@0
|
658 // Check the param type value. This could also be multiple parameter
|
Chris@0
|
659 // types separated by '|'.
|
Chris@0
|
660 $typeNames = explode('|', $param['type']);
|
Chris@0
|
661 $suggestedNames = array();
|
Chris@0
|
662 foreach ($typeNames as $i => $typeName) {
|
Chris@0
|
663 $suggestedNames[] = static::suggestType($typeName);
|
Chris@0
|
664 }
|
Chris@0
|
665
|
Chris@0
|
666 $suggestedType = implode('|', $suggestedNames);
|
Chris@0
|
667 if (preg_match('/\s/', $param['type']) === 1) {
|
Chris@0
|
668 $error = 'Parameter type "%s" must not contain spaces';
|
Chris@0
|
669 $data = array($param['type']);
|
Chris@0
|
670 $phpcsFile->addError($error, $param['tag'], 'ParamTypeSpaces', $data);
|
Chris@0
|
671 } else if ($param['type'] !== $suggestedType) {
|
Chris@0
|
672 $error = 'Expected "%s" but found "%s" for parameter type';
|
Chris@0
|
673 $data = array(
|
Chris@0
|
674 $suggestedType,
|
Chris@0
|
675 $param['type'],
|
Chris@0
|
676 );
|
Chris@0
|
677 $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data);
|
Chris@0
|
678 if ($fix === true) {
|
Chris@0
|
679 $content = $suggestedType;
|
Chris@0
|
680 $content .= str_repeat(' ', $param['type_space']);
|
Chris@0
|
681 $content .= $param['var'];
|
Chris@0
|
682 $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content);
|
Chris@0
|
683 }
|
Chris@0
|
684 }
|
Chris@0
|
685
|
Chris@0
|
686 if (count($typeNames) === 1) {
|
Chris@0
|
687 $typeName = $param['type'];
|
Chris@0
|
688 $suggestedName = static::suggestType($typeName);
|
Chris@0
|
689 }
|
Chris@0
|
690
|
Chris@0
|
691 // This runs only if there is only one type name and the type name
|
Chris@0
|
692 // is not one of the disallowed type names.
|
Chris@0
|
693 if (count($typeNames) === 1 && $typeName === $suggestedName) {
|
Chris@0
|
694 // Check type hint for array and custom type.
|
Chris@0
|
695 $suggestedTypeHint = '';
|
Chris@0
|
696 if (strpos($suggestedName, 'array') !== false) {
|
Chris@0
|
697 $suggestedTypeHint = 'array';
|
Chris@0
|
698 } else if (strpos($suggestedName, 'callable') !== false) {
|
Chris@0
|
699 $suggestedTypeHint = 'callable';
|
Chris@0
|
700 } else if (substr($suggestedName, -2) === '[]') {
|
Chris@0
|
701 $suggestedTypeHint = 'array';
|
Chris@0
|
702 } else if ($suggestedName === 'object') {
|
Chris@0
|
703 $suggestedTypeHint = '';
|
Chris@0
|
704 } else if (in_array($typeName, $this->allowedTypes) === false) {
|
Chris@0
|
705 $suggestedTypeHint = $suggestedName;
|
Chris@0
|
706 }
|
Chris@0
|
707
|
Chris@0
|
708 if ($suggestedTypeHint !== '' && isset($realParams[$checkPos]) === true) {
|
Chris@0
|
709 $typeHint = $realParams[$checkPos]['type_hint'];
|
Chris@0
|
710 // Primitive type hints are allowed to be omitted.
|
Chris@0
|
711 if ($typeHint === '' && in_array($suggestedTypeHint, ['string', 'int', 'float', 'bool']) === false) {
|
Chris@0
|
712 $error = 'Type hint "%s" missing for %s';
|
Chris@0
|
713 $data = array(
|
Chris@0
|
714 $suggestedTypeHint,
|
Chris@0
|
715 $param['var'],
|
Chris@0
|
716 );
|
Chris@0
|
717 $phpcsFile->addError($error, $stackPtr, 'TypeHintMissing', $data);
|
Chris@0
|
718 } else if ($typeHint !== $suggestedTypeHint && $typeHint !== '') {
|
Chris@0
|
719 // The type hint could be fully namespaced, so we check
|
Chris@0
|
720 // for the part after the last "\".
|
Chris@0
|
721 $name_parts = explode('\\', $suggestedTypeHint);
|
Chris@0
|
722 $last_part = end($name_parts);
|
Chris@0
|
723 if ($last_part !== $typeHint && $this->isAliasedType($typeHint, $suggestedTypeHint, $phpcsFile) === false) {
|
Chris@0
|
724 $error = 'Expected type hint "%s"; found "%s" for %s';
|
Chris@0
|
725 $data = array(
|
Chris@0
|
726 $last_part,
|
Chris@0
|
727 $typeHint,
|
Chris@0
|
728 $param['var'],
|
Chris@0
|
729 );
|
Chris@0
|
730 $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data);
|
Chris@0
|
731 }
|
Chris@0
|
732 }//end if
|
Chris@0
|
733 } else if ($suggestedTypeHint === ''
|
Chris@0
|
734 && isset($realParams[$checkPos]) === true
|
Chris@0
|
735 ) {
|
Chris@0
|
736 $typeHint = $realParams[$checkPos]['type_hint'];
|
Chris@0
|
737 if ($typeHint !== '' && $typeHint !== 'stdClass') {
|
Chris@0
|
738 $error = 'Unknown type hint "%s" found for %s';
|
Chris@0
|
739 $data = array(
|
Chris@0
|
740 $typeHint,
|
Chris@0
|
741 $param['var'],
|
Chris@0
|
742 );
|
Chris@0
|
743 $phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data);
|
Chris@0
|
744 }
|
Chris@0
|
745 }//end if
|
Chris@0
|
746 }//end if
|
Chris@0
|
747
|
Chris@0
|
748 // Check number of spaces after the type.
|
Chris@0
|
749 $spaces = 1;
|
Chris@0
|
750 if ($param['type_space'] !== $spaces) {
|
Chris@0
|
751 $error = 'Expected %s spaces after parameter type; %s found';
|
Chris@0
|
752 $data = array(
|
Chris@0
|
753 $spaces,
|
Chris@0
|
754 $param['type_space'],
|
Chris@0
|
755 );
|
Chris@0
|
756
|
Chris@0
|
757 $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data);
|
Chris@0
|
758 if ($fix === true) {
|
Chris@0
|
759 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
760
|
Chris@0
|
761 $content = $param['type'];
|
Chris@0
|
762 $content .= str_repeat(' ', $spaces);
|
Chris@0
|
763 $content .= $param['var'];
|
Chris@0
|
764 $content .= str_repeat(' ', $param['var_space']);
|
Chris@0
|
765 // At this point there is no description expected in the
|
Chris@0
|
766 // @param line so no need to append comment.
|
Chris@0
|
767 $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content);
|
Chris@0
|
768
|
Chris@0
|
769 // Fix up the indent of additional comment lines.
|
Chris@0
|
770 foreach ($param['commentLines'] as $lineNum => $line) {
|
Chris@0
|
771 if ($lineNum === 0
|
Chris@0
|
772 || $param['commentLines'][$lineNum]['indent'] === 0
|
Chris@0
|
773 ) {
|
Chris@0
|
774 continue;
|
Chris@0
|
775 }
|
Chris@0
|
776
|
Chris@0
|
777 $newIndent = ($param['commentLines'][$lineNum]['indent'] + $spaces - $param['type_space']);
|
Chris@0
|
778 $phpcsFile->fixer->replaceToken(
|
Chris@0
|
779 ($param['commentLines'][$lineNum]['token'] - 1),
|
Chris@0
|
780 str_repeat(' ', $newIndent)
|
Chris@0
|
781 );
|
Chris@0
|
782 }
|
Chris@0
|
783
|
Chris@0
|
784 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
785 }//end if
|
Chris@0
|
786 }//end if
|
Chris@0
|
787
|
Chris@0
|
788 if ($matched === false) {
|
Chris@0
|
789 if ($checkPos >= $pos) {
|
Chris@0
|
790 $code = 'ParamNameNoMatch';
|
Chris@0
|
791 $data = array(
|
Chris@0
|
792 $param['var'],
|
Chris@0
|
793 $realName,
|
Chris@0
|
794 );
|
Chris@0
|
795
|
Chris@0
|
796 $error = 'Doc comment for parameter %s does not match ';
|
Chris@0
|
797 if (strtolower($param['var']) === strtolower($realName)) {
|
Chris@0
|
798 $error .= 'case of ';
|
Chris@0
|
799 $code = 'ParamNameNoCaseMatch';
|
Chris@0
|
800 }
|
Chris@0
|
801
|
Chris@0
|
802 $error .= 'actual variable name %s';
|
Chris@0
|
803
|
Chris@0
|
804 $phpcsFile->addError($error, $param['tag'], $code, $data);
|
Chris@0
|
805 // Reset the parameter position to check for following
|
Chris@0
|
806 // parameters.
|
Chris@0
|
807 $checkPos = ($pos - 1);
|
Chris@0
|
808 } else if (substr($param['var'], -4) !== ',...') {
|
Chris@0
|
809 // We must have an extra parameter comment.
|
Chris@0
|
810 $error = 'Superfluous parameter comment';
|
Chris@0
|
811 $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment');
|
Chris@0
|
812 }//end if
|
Chris@0
|
813 }//end if
|
Chris@0
|
814
|
Chris@0
|
815 $checkPos++;
|
Chris@0
|
816
|
Chris@0
|
817 if ($param['comment'] === '') {
|
Chris@0
|
818 continue;
|
Chris@0
|
819 }
|
Chris@0
|
820
|
Chris@0
|
821 // Param comments must start with a capital letter and end with the full stop.
|
Chris@0
|
822 if (isset($param['commentLines'][0]['comment']) === true) {
|
Chris@0
|
823 $firstChar = $param['commentLines'][0]['comment'];
|
Chris@0
|
824 } else {
|
Chris@0
|
825 $firstChar = $param['comment'];
|
Chris@0
|
826 }
|
Chris@0
|
827
|
Chris@0
|
828 if (preg_match('|\p{Lu}|u', $firstChar) === 0) {
|
Chris@0
|
829 $error = 'Parameter comment must start with a capital letter';
|
Chris@0
|
830 if (isset($param['commentLines'][0]['token']) === true) {
|
Chris@0
|
831 $commentToken = $param['commentLines'][0]['token'];
|
Chris@0
|
832 } else {
|
Chris@0
|
833 $commentToken = $param['tag'];
|
Chris@0
|
834 }
|
Chris@0
|
835
|
Chris@0
|
836 $phpcsFile->addError($error, $commentToken, 'ParamCommentNotCapital');
|
Chris@0
|
837 }
|
Chris@0
|
838
|
Chris@0
|
839 $lastChar = substr($param['comment'], -1);
|
Chris@0
|
840 if (in_array($lastChar, array('.', '!', '?', ')')) === false) {
|
Chris@0
|
841 $error = 'Parameter comment must end with a full stop';
|
Chris@0
|
842 if (empty($param['commentLines']) === true) {
|
Chris@0
|
843 $commentToken = ($param['tag'] + 2);
|
Chris@0
|
844 } else {
|
Chris@0
|
845 $lastLine = end($param['commentLines']);
|
Chris@0
|
846 $commentToken = $lastLine['token'];
|
Chris@0
|
847 }
|
Chris@0
|
848
|
Chris@0
|
849 $fix = $phpcsFile->addFixableError($error, $commentToken, 'ParamCommentFullStop');
|
Chris@0
|
850 if ($fix === true) {
|
Chris@0
|
851 // Add a full stop as the last character of the comment.
|
Chris@0
|
852 $phpcsFile->fixer->addContent($commentToken, '.');
|
Chris@0
|
853 }
|
Chris@0
|
854 }
|
Chris@0
|
855 }//end foreach
|
Chris@0
|
856
|
Chris@0
|
857 // Missing parameters only apply to methods and not function because on
|
Chris@0
|
858 // functions it is allowed to leave out param comments for form constructors
|
Chris@0
|
859 // for example.
|
Chris@0
|
860 // It is also allowed to ommit pram tags completely, in which case we don't
|
Chris@0
|
861 // throw errors. Only throw errors if param comments exists but are
|
Chris@0
|
862 // incomplete on class methods.
|
Chris@0
|
863 if ($tokens[$stackPtr]['level'] > 0 && empty($foundParams) === false) {
|
Chris@0
|
864 foreach ($realParams as $realParam) {
|
Chris@0
|
865 $realParamKeyName = $realParam['name'];
|
Chris@0
|
866 if (in_array($realParamKeyName, $foundParams) === false
|
Chris@0
|
867 && ($realParam['pass_by_reference'] === true
|
Chris@0
|
868 && in_array("&$realParamKeyName", $foundParams) === true) === false
|
Chris@0
|
869 ) {
|
Chris@0
|
870 $error = 'Parameter %s is not described in comment';
|
Chris@0
|
871 $phpcsFile->addError($error, $commentStart, 'ParamMissingDefinition', [$realParam['name']]);
|
Chris@0
|
872 }
|
Chris@0
|
873 }
|
Chris@0
|
874 }
|
Chris@0
|
875
|
Chris@0
|
876 }//end processParams()
|
Chris@0
|
877
|
Chris@0
|
878
|
Chris@0
|
879 /**
|
Chris@0
|
880 * Process the function "see" comments.
|
Chris@0
|
881 *
|
Chris@17
|
882 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
Chris@17
|
883 * @param int $stackPtr The position of the current token
|
Chris@17
|
884 * in the stack passed in $tokens.
|
Chris@17
|
885 * @param int $commentStart The position in the stack where the comment started.
|
Chris@0
|
886 *
|
Chris@0
|
887 * @return void
|
Chris@0
|
888 */
|
Chris@17
|
889 protected function processSees(File $phpcsFile, $stackPtr, $commentStart)
|
Chris@0
|
890 {
|
Chris@0
|
891 $tokens = $phpcsFile->getTokens();
|
Chris@0
|
892 foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
|
Chris@0
|
893 if ($tokens[$tag]['content'] !== '@see') {
|
Chris@0
|
894 continue;
|
Chris@0
|
895 }
|
Chris@0
|
896
|
Chris@0
|
897 if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
|
Chris@0
|
898 $comment = $tokens[($tag + 2)]['content'];
|
Chris@0
|
899 if (strpos($comment, ' ') !== false) {
|
Chris@0
|
900 $error = 'The @see reference should not contain any additional text';
|
Chris@0
|
901 $phpcsFile->addError($error, $tag, 'SeeAdditionalText');
|
Chris@0
|
902 continue;
|
Chris@0
|
903 }
|
Chris@0
|
904
|
Chris@0
|
905 if (preg_match('/[\.!\?]$/', $comment) === 1) {
|
Chris@0
|
906 $error = 'Trailing punctuation for @see references is not allowed.';
|
Chris@0
|
907 $fix = $phpcsFile->addFixableError($error, $tag, 'SeePunctuation');
|
Chris@0
|
908 if ($fix === true) {
|
Chris@0
|
909 // Replace the last character from the comment which is
|
Chris@0
|
910 // already tested to be a punctuation.
|
Chris@0
|
911 $content = substr($comment, 0, -1);
|
Chris@0
|
912 $phpcsFile->fixer->replaceToken(($tag + 2), $content);
|
Chris@0
|
913 }//end if
|
Chris@0
|
914 }
|
Chris@0
|
915 }
|
Chris@0
|
916 }//end foreach
|
Chris@0
|
917
|
Chris@0
|
918 }//end processSees()
|
Chris@0
|
919
|
Chris@0
|
920
|
Chris@0
|
921 /**
|
Chris@0
|
922 * Returns a valid variable type for param/var tag.
|
Chris@0
|
923 *
|
Chris@0
|
924 * @param string $type The variable type to process.
|
Chris@0
|
925 *
|
Chris@0
|
926 * @return string
|
Chris@0
|
927 */
|
Chris@0
|
928 public static function suggestType($type)
|
Chris@0
|
929 {
|
Chris@0
|
930 if (isset(static::$invalidTypes[$type]) === true) {
|
Chris@0
|
931 return static::$invalidTypes[$type];
|
Chris@0
|
932 }
|
Chris@0
|
933
|
Chris@0
|
934 if ($type === '$this') {
|
Chris@0
|
935 return $type;
|
Chris@0
|
936 }
|
Chris@0
|
937
|
Chris@0
|
938 $type = preg_replace('/[^a-zA-Z0-9_\\\[\]]/', '', $type);
|
Chris@0
|
939
|
Chris@0
|
940 return $type;
|
Chris@0
|
941
|
Chris@0
|
942 }//end suggestType()
|
Chris@0
|
943
|
Chris@0
|
944
|
Chris@0
|
945 /**
|
Chris@0
|
946 * Checks if a used type hint is an alias defined by a "use" statement.
|
Chris@0
|
947 *
|
Chris@17
|
948 * @param string $typeHint The type hint used.
|
Chris@17
|
949 * @param string $suggestedTypeHint The fully qualified type to
|
Chris@17
|
950 * check against.
|
Chris@17
|
951 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being checked.
|
Chris@0
|
952 *
|
Chris@0
|
953 * @return boolean
|
Chris@0
|
954 */
|
Chris@17
|
955 protected function isAliasedType($typeHint, $suggestedTypeHint, File $phpcsFile)
|
Chris@0
|
956 {
|
Chris@0
|
957 $tokens = $phpcsFile->getTokens();
|
Chris@0
|
958
|
Chris@0
|
959 // Iterate over all "use" statements in the file.
|
Chris@0
|
960 $usePtr = 0;
|
Chris@0
|
961 while ($usePtr !== false) {
|
Chris@0
|
962 $usePtr = $phpcsFile->findNext(T_USE, ($usePtr + 1));
|
Chris@0
|
963 if ($usePtr === false) {
|
Chris@0
|
964 return false;
|
Chris@0
|
965 }
|
Chris@0
|
966
|
Chris@0
|
967 // Only check use statements in the global scope.
|
Chris@0
|
968 if (empty($tokens[$usePtr]['conditions']) === false) {
|
Chris@0
|
969 continue;
|
Chris@0
|
970 }
|
Chris@0
|
971
|
Chris@0
|
972 // Now comes the original class name, possibly with namespace
|
Chris@0
|
973 // backslashes.
|
Chris@17
|
974 $originalClass = $phpcsFile->findNext(Tokens::$emptyTokens, ($usePtr + 1), null, true);
|
Chris@0
|
975 if ($originalClass === false || ($tokens[$originalClass]['code'] !== T_STRING
|
Chris@0
|
976 && $tokens[$originalClass]['code'] !== T_NS_SEPARATOR)
|
Chris@0
|
977 ) {
|
Chris@0
|
978 continue;
|
Chris@0
|
979 }
|
Chris@0
|
980
|
Chris@0
|
981 $originalClassName = '';
|
Chris@0
|
982 while (in_array($tokens[$originalClass]['code'], array(T_STRING, T_NS_SEPARATOR)) === true) {
|
Chris@0
|
983 $originalClassName .= $tokens[$originalClass]['content'];
|
Chris@0
|
984 $originalClass++;
|
Chris@0
|
985 }
|
Chris@0
|
986
|
Chris@0
|
987 if (ltrim($originalClassName, '\\') !== ltrim($suggestedTypeHint, '\\')) {
|
Chris@0
|
988 continue;
|
Chris@0
|
989 }
|
Chris@0
|
990
|
Chris@0
|
991 // Now comes the "as" keyword signaling an alias name for the class.
|
Chris@17
|
992 $asPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($originalClass + 1), null, true);
|
Chris@0
|
993 if ($asPtr === false || $tokens[$asPtr]['code'] !== T_AS) {
|
Chris@0
|
994 continue;
|
Chris@0
|
995 }
|
Chris@0
|
996
|
Chris@0
|
997 // Now comes the name the class is aliased to.
|
Chris@17
|
998 $aliasPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($asPtr + 1), null, true);
|
Chris@0
|
999 if ($aliasPtr === false || $tokens[$aliasPtr]['code'] !== T_STRING
|
Chris@0
|
1000 || $tokens[$aliasPtr]['content'] !== $typeHint
|
Chris@0
|
1001 ) {
|
Chris@0
|
1002 continue;
|
Chris@0
|
1003 }
|
Chris@0
|
1004
|
Chris@0
|
1005 // We found a use statement that aliases the used type hint!
|
Chris@0
|
1006 return true;
|
Chris@0
|
1007 }//end while
|
Chris@0
|
1008
|
Chris@0
|
1009 return false;
|
Chris@0
|
1010
|
Chris@0
|
1011 }//end isAliasedType()
|
Chris@0
|
1012
|
Chris@0
|
1013
|
Chris@0
|
1014 }//end class
|