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