12 use PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FunctionCommentSniff as PEARFunctionCommentSniff;
13 use PHP_CodeSniffer\Files\File;
14 use PHP_CodeSniffer\Config;
15 use PHP_CodeSniffer\Util\Common;
25 private $phpVersion =
null;
38 protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
40 $tokens = $phpcsFile->getTokens();
43 $methodName = $phpcsFile->getDeclarationName($stackPtr);
44 $isSpecialMethod = ($methodName ===
'__construct' || $methodName ===
'__destruct');
47 foreach (
$tokens[$commentStart][
'comment_tags'] as $tag) {
48 if (
$tokens[$tag][
'content'] ===
'@return') {
49 if ($return !==
null) {
50 $error =
'Only 1 @return tag is allowed in a function comment';
51 $phpcsFile->addError($error, $tag,
'DuplicateReturn');
60 if ($isSpecialMethod ===
true) {
64 if ($return !==
null) {
66 if (empty(
$content) ===
true ||
$tokens[($return + 2)][
'code'] !== T_DOC_COMMENT_STRING) {
67 $error =
'Return type missing for @return tag in function comment';
68 $phpcsFile->addError($error, $return,
'MissingReturnType');
71 $split = preg_match(
'`^((?:\|?(?:array\([^\)]*\)|[\\\\a-z0-9\[\]]+))*)( .*)?`i',
$content, $returnParts);
72 if (isset($returnParts[1]) ===
false) {
76 $returnType = $returnParts[1];
79 $typeNames = explode(
'|', $returnType);
80 $suggestedNames = array();
81 foreach ($typeNames as
$i => $typeName) {
82 $suggestedName = Common::suggestType($typeName);
83 if (in_array($suggestedName, $suggestedNames) ===
false) {
84 $suggestedNames[] = $suggestedName;
88 $suggestedType = implode(
'|', $suggestedNames);
89 if ($returnType !== $suggestedType) {
90 $error =
'Expected "%s" but found "%s" for function return type';
95 $fix = $phpcsFile->addFixableError($error, $return,
'InvalidReturn',
$data);
98 if (empty($returnParts[2]) ===
false) {
102 $phpcsFile->fixer->replaceToken(($return + 2),
$replacement);
109 if ($returnType ===
'void') {
110 if (isset(
$tokens[$stackPtr][
'scope_closer']) ===
true) {
111 $endToken =
$tokens[$stackPtr][
'scope_closer'];
112 for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) {
113 if (
$tokens[$returnToken][
'code'] === T_CLOSURE
114 ||
$tokens[$returnToken][
'code'] === T_ANON_CLASS
116 $returnToken =
$tokens[$returnToken][
'scope_closer'];
120 if (
$tokens[$returnToken][
'code'] === T_RETURN
121 ||
$tokens[$returnToken][
'code'] === T_YIELD
122 ||
$tokens[$returnToken][
'code'] === T_YIELD_FROM
128 if ($returnToken !== $endToken) {
131 $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1),
null,
true);
132 if (
$tokens[$semicolon][
'code'] !== T_SEMICOLON) {
133 $error =
'Function return type is void, but function contains return statement';
134 $phpcsFile->addError($error, $return,
'InvalidReturnVoid');
138 }
else if ($returnType !==
'mixed' && in_array(
'void', $typeNames,
true) ===
false) {
141 if (isset(
$tokens[$stackPtr][
'scope_closer']) ===
true) {
142 $endToken =
$tokens[$stackPtr][
'scope_closer'];
143 $returnToken = $phpcsFile->findNext(array(T_RETURN, T_YIELD, T_YIELD_FROM), $stackPtr, $endToken);
144 if ($returnToken ===
false) {
145 $error =
'Function return type is not void, but function has no return statement';
146 $phpcsFile->addError($error, $return,
'InvalidNoReturn');
148 $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1),
null,
true);
149 if (
$tokens[$semicolon][
'code'] === T_SEMICOLON) {
150 $error =
'Function return type is not void, but function is returning void here';
151 $phpcsFile->addError($error, $returnToken,
'InvalidReturnNotVoid');
158 $error =
'Missing @return tag in function comment';
159 $phpcsFile->addError($error,
$tokens[$commentStart][
'comment_closer'],
'MissingReturn');
177 $tokens = $phpcsFile->getTokens();
180 foreach (
$tokens[$commentStart][
'comment_tags'] as
$pos => $tag) {
181 if (
$tokens[$tag][
'content'] !==
'@throws') {
187 if (
$tokens[($tag + 2)][
'code'] === T_DOC_COMMENT_STRING) {
189 preg_match(
'/([^\s]+)(?:\s+(.*))?/',
$tokens[($tag + 2)][
'content'], $matches);
190 $exception = $matches[1];
191 if (isset($matches[2]) ===
true && trim($matches[2]) !==
'') {
192 $comment = $matches[2];
196 if ($exception ===
null) {
197 $error =
'Exception type and comment missing for @throws tag in function comment';
198 $phpcsFile->addError($error, $tag,
'InvalidThrows');
199 }
else if ($comment ===
null) {
200 $error =
'Comment missing for @throws tag in function comment';
204 if (isset(
$tokens[$commentStart][
'comment_tags'][(
$pos + 1)]) ===
true) {
205 $end =
$tokens[$commentStart][
'comment_tags'][(
$pos + 1)];
207 $end =
$tokens[$commentStart][
'comment_closer'];
210 for (
$i = ($tag + 3);
$i < $end;
$i++) {
211 if (
$tokens[
$i][
'code'] === T_DOC_COMMENT_STRING) {
212 $comment .=
' '.$tokens[
$i][
'content'];
217 $firstChar = $comment{0};
218 if (strtoupper($firstChar) !== $firstChar) {
219 $error =
'@throws tag comment must start with a capital letter';
220 $phpcsFile->addError($error, ($tag + 2),
'ThrowsNotCapital');
223 $lastChar = substr($comment, -1);
224 if ($lastChar !==
'.') {
225 $error =
'@throws tag comment must end with a full stop';
226 $phpcsFile->addError($error, ($tag + 2),
'ThrowsNoFullStop');
246 if ($this->phpVersion ===
null) {
247 $this->phpVersion = Config::getConfigData(
'php_version');
248 if ($this->phpVersion ===
null) {
249 $this->phpVersion = PHP_VERSION_ID;
253 $tokens = $phpcsFile->getTokens();
258 foreach (
$tokens[$commentStart][
'comment_tags'] as
$pos => $tag) {
259 if (
$tokens[$tag][
'content'] !==
'@param') {
268 $commentLines = array();
269 if (
$tokens[($tag + 2)][
'code'] === T_DOC_COMMENT_STRING) {
271 preg_match(
'/([^$&.]+)(?:((?:\.\.\.)?(?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/',
$tokens[($tag + 2)][
'content'], $matches);
273 if (empty($matches) ===
false) {
274 $typeLen = strlen($matches[1]);
275 $type = trim($matches[1]);
276 $typeSpace = ($typeLen - strlen(
$type));
277 $typeLen = strlen(
$type);
278 if ($typeLen > $maxType) {
283 if (isset($matches[2]) ===
true) {
285 $varLen = strlen($var);
286 if ($varLen > $maxVar) {
290 if (isset($matches[4]) ===
true) {
291 $varSpace = strlen($matches[3]);
292 $comment = $matches[4];
293 $commentLines[] = array(
294 'comment' => $comment,
295 'token' => ($tag + 2),
296 'indent' => $varSpace,
300 if (isset(
$tokens[$commentStart][
'comment_tags'][(
$pos + 1)]) ===
true) {
301 $end =
$tokens[$commentStart][
'comment_tags'][(
$pos + 1)];
303 $end =
$tokens[$commentStart][
'comment_closer'];
306 for (
$i = ($tag + 3);
$i < $end;
$i++) {
307 if (
$tokens[
$i][
'code'] === T_DOC_COMMENT_STRING) {
309 if (
$tokens[(
$i - 1)][
'code'] === T_DOC_COMMENT_WHITESPACE) {
310 $indent = strlen(
$tokens[(
$i - 1)][
'content']);
313 $comment .=
' '.$tokens[
$i][
'content'];
314 $commentLines[] = array(
322 $error =
'Missing parameter comment';
324 $commentLines[] = array(
'comment' =>
'');
327 $error =
'Missing parameter name';
328 $phpcsFile->addError($error, $tag,
'MissingParamName');
331 $error =
'Missing parameter type';
332 $phpcsFile->addError($error, $tag,
'MissingParamType');
339 'comment' => $comment,
340 'commentLines' => $commentLines,
341 'type_space' => $typeSpace,
342 'var_space' => $varSpace,
346 $realParams = $phpcsFile->getMethodParameters($stackPtr);
347 $foundParams = array();
351 foreach ($realParams as
$pos => $param) {
352 if ($param[
'variable_length'] ===
true) {
353 $realParams[
$pos][
'name'] =
'...'.$realParams[
$pos][
'name'];
359 if ($param[
'type'] ===
'') {
364 $typeNames = explode(
'|', $param[
'type']);
365 $suggestedTypeNames = array();
367 foreach ($typeNames as $typeName) {
368 $suggestedName = Common::suggestType($typeName);
369 $suggestedTypeNames[] = $suggestedName;
371 if (count($typeNames) > 1) {
376 $suggestedTypeHint =
'';
377 if (strpos($suggestedName,
'array') !==
false || substr($suggestedName, -2) ===
'[]') {
378 $suggestedTypeHint =
'array';
379 }
else if (strpos($suggestedName,
'callable') !==
false) {
380 $suggestedTypeHint =
'callable';
381 }
else if (strpos($suggestedName,
'callback') !==
false) {
382 $suggestedTypeHint =
'callable';
383 }
else if (in_array($suggestedName, Common::$allowedTypes) ===
false) {
384 $suggestedTypeHint = $suggestedName;
387 if ($this->phpVersion >= 70000) {
388 if ($suggestedName ===
'string') {
389 $suggestedTypeHint =
'string';
390 }
else if ($suggestedName ===
'int' || $suggestedName ===
'integer') {
391 $suggestedTypeHint =
'int';
392 }
else if ($suggestedName ===
'float') {
393 $suggestedTypeHint =
'float';
394 }
else if ($suggestedName ===
'bool' || $suggestedName ===
'boolean') {
395 $suggestedTypeHint =
'bool';
399 if ($suggestedTypeHint !==
'' && isset($realParams[
$pos]) ===
true) {
400 $typeHint = $realParams[
$pos][
'type_hint'];
401 if ($typeHint ===
'') {
402 $error =
'Type hint "%s" missing for %s';
408 $errorCode =
'TypeHintMissing';
409 if ($suggestedTypeHint ===
'string' 410 || $suggestedTypeHint ===
'int' 411 || $suggestedTypeHint ===
'float' 412 || $suggestedTypeHint ===
'bool' 414 $errorCode =
'Scalar'.$errorCode;
418 }
else if ($typeHint !== substr($suggestedTypeHint, (strlen($typeHint) * -1))) {
419 $error =
'Expected type hint "%s"; found "%s" for %s';
425 $phpcsFile->addError($error, $stackPtr,
'IncorrectTypeHint',
$data);
427 }
else if ($suggestedTypeHint ===
'' && isset($realParams[
$pos]) ===
true) {
428 $typeHint = $realParams[
$pos][
'type_hint'];
429 if ($typeHint !==
'') {
430 $error =
'Unknown type hint "%s" found for %s';
435 $phpcsFile->addError($error, $stackPtr,
'InvalidTypeHint',
$data);
440 $suggestedType = implode($suggestedTypeNames,
'|');
441 if ($param[
'type'] !== $suggestedType) {
442 $error =
'Expected "%s" but found "%s" for parameter type';
448 $fix = $phpcsFile->addFixableError($error, $param[
'tag'],
'IncorrectParamVarName',
$data);
450 $phpcsFile->fixer->beginChangeset();
453 $content .= str_repeat(
' ', $param[
'type_space']);
455 $content .= str_repeat(
' ', $param[
'var_space']);
456 if (isset($param[
'commentLines'][0]) ===
true) {
457 $content .= $param[
'commentLines'][0][
'comment'];
460 $phpcsFile->fixer->replaceToken(($param[
'tag'] + 2),
$content);
463 foreach ($param[
'commentLines'] as $lineNum => $line) {
465 || $param[
'commentLines'][$lineNum][
'indent'] === 0
470 $diff = (strlen($param[
'type']) - strlen($suggestedType));
471 $newIndent = ($param[
'commentLines'][$lineNum][
'indent'] - $diff);
472 $phpcsFile->fixer->replaceToken(
473 ($param[
'commentLines'][$lineNum][
'token'] - 1),
474 str_repeat(
' ', $newIndent)
478 $phpcsFile->fixer->endChangeset();
482 if ($param[
'var'] ===
'') {
486 $foundParams[] = $param[
'var'];
492 if (isset($realParams[
$pos]) ===
true) {
493 $realName = $realParams[
$pos][
'name'];
494 if ($realName !== $param[
'var']) {
495 $code =
'ParamNameNoMatch';
501 $error =
'Doc comment for parameter %s does not match ';
502 if (strtolower($param[
'var']) === strtolower($realName)) {
503 $error .=
'case of ';
504 $code =
'ParamNameNoCaseMatch';
507 $error .=
'actual variable name %s';
509 $phpcsFile->addError($error, $param[
'tag'],
$code,
$data);
511 }
else if (substr($param[
'var'], -4) !==
',...') {
513 $error =
'Superfluous parameter comment';
514 $phpcsFile->addError($error, $param[
'tag'],
'ExtraParamComment');
517 if ($param[
'comment'] ===
'') {
525 if (preg_match(
'/^(\p{Ll}|\P{L})/u', $param[
'comment']) === 1) {
526 $error =
'Parameter comment must start with a capital letter';
527 $phpcsFile->addError($error, $param[
'tag'],
'ParamCommentNotCapital');
530 $lastChar = substr($param[
'comment'], -1);
531 if ($lastChar !==
'.') {
532 $error =
'Parameter comment must end with a full stop';
533 $phpcsFile->addError($error, $param[
'tag'],
'ParamCommentFullStop');
537 $realNames = array();
538 foreach ($realParams as $realParam) {
539 $realNames[] = $realParam[
'name'];
543 $diff = array_diff($realNames, $foundParams);
544 foreach ($diff as $neededParam) {
545 $error =
'Doc comment for parameter "%s" missing';
546 $data = array($neededParam);
566 $spaces = ($maxType - strlen($param[
'type']) + $spacing);
567 if ($param[
'type_space'] !== $spaces) {
568 $error =
'Expected %s spaces after parameter type; %s found';
571 $param[
'type_space'],
574 $fix = $phpcsFile->addFixableError($error, $param[
'tag'],
'SpacingAfterParamType',
$data);
576 $phpcsFile->fixer->beginChangeset();
579 $content .= str_repeat(
' ', $spaces);
581 $content .= str_repeat(
' ', $param[
'var_space']);
582 $content .= $param[
'commentLines'][0][
'comment'];
583 $phpcsFile->fixer->replaceToken(($param[
'tag'] + 2),
$content);
586 foreach ($param[
'commentLines'] as $lineNum => $line) {
588 || $param[
'commentLines'][$lineNum][
'indent'] === 0
593 $diff = ($param[
'type_space'] - $spaces);
594 $newIndent = ($param[
'commentLines'][$lineNum][
'indent'] - $diff);
595 $phpcsFile->fixer->replaceToken(
596 ($param[
'commentLines'][$lineNum][
'token'] - 1),
597 str_repeat(
' ', $newIndent)
601 $phpcsFile->fixer->endChangeset();
621 $spaces = ($maxVar - strlen($param[
'var']) + $spacing);
622 if ($param[
'var_space'] !== $spaces) {
623 $error =
'Expected %s spaces after parameter name; %s found';
629 $fix = $phpcsFile->addFixableError($error, $param[
'tag'],
'SpacingAfterParamName',
$data);
631 $phpcsFile->fixer->beginChangeset();
634 $content .= str_repeat(
' ', $param[
'type_space']);
636 $content .= str_repeat(
' ', $spaces);
637 $content .= $param[
'commentLines'][0][
'comment'];
638 $phpcsFile->fixer->replaceToken(($param[
'tag'] + 2),
$content);
641 foreach ($param[
'commentLines'] as $lineNum => $line) {
643 || $param[
'commentLines'][$lineNum][
'indent'] === 0
648 $diff = ($param[
'var_space'] - $spaces);
649 $newIndent = ($param[
'commentLines'][$lineNum][
'indent'] - $diff);
650 $phpcsFile->fixer->replaceToken(
651 ($param[
'commentLines'][$lineNum][
'token'] - 1),
652 str_repeat(
' ', $newIndent)
656 $phpcsFile->fixer->endChangeset();
$params[\Magento\Store\Model\StoreManager::PARAM_RUN_CODE]