* @author Marc McIntyre * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence * @link http://pear.php.net/package/PHP_CodeSniffer */ if (class_exists('PEAR_Sniffs_Commenting_FunctionCommentSniff', true) === false) { throw new PHP_CodeSniffer_Exception('Class PEAR_Sniffs_Commenting_FunctionCommentSniff not found'); } /** * Parses and verifies the doc comments for functions. * * Verifies that : * * * @category PHP * @package PHP_CodeSniffer * @author Greg Sherwood * @author Marc McIntyre * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence * @version Release: @package_version@ * @link http://pear.php.net/package/PHP_CodeSniffer */ class FHComplete_Sniffs_Commenting_FunctionCommentSniff extends PEAR_Sniffs_Commenting_FunctionCommentSniff { /** * Is the comment an inheritdoc? * * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * * @return boolean True if the comment is an inheritdoc */ protected function isInheritDoc(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { $start = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $stackPtr - 1); $end = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, $start); $content = $phpcsFile->getTokensAsString($start, ($end - $start)); return preg_match('#{@inheritDoc}#', $content) === 1; } // end isInheritDoc() /** * Process the return comment of this function comment. * * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * @param int $commentStart The position in the stack where the comment started. * * @return void */ protected function processReturn(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) { if ($this->isInheritDoc($phpcsFile, $stackPtr)) { return; } $tokens = $phpcsFile->getTokens(); // Skip constructor and destructor. $className = ''; foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) { if ($condition === T_CLASS || $condition === T_INTERFACE) { $className = $phpcsFile->getDeclarationName($condPtr); $className = strtolower(ltrim($className, '_')); } } $methodName = $phpcsFile->getDeclarationName($stackPtr); $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct'); if ($methodName !== '_') { $methodName = strtolower(ltrim($methodName, '_')); } $return = null; foreach ($tokens[$commentStart]['comment_tags'] as $tag) { if ($tokens[$tag]['content'] === '@return') { if ($return !== null) { $error = 'Only 1 @return tag is allowed in a function comment'; $phpcsFile->addError($error, $tag, 'DuplicateReturn'); return; } $return = $tag; } } if ($isSpecialMethod === true) { return; } if ($return !== null) { $content = $tokens[($return + 2)]['content']; if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) { $error = 'Return type missing for @return tag in function comment'; $phpcsFile->addError($error, $return, 'MissingReturnType'); } else { // Check return type (can be multiple, separated by '|'). $typeNames = explode('|', $content); $suggestedNames = array(); foreach ($typeNames as $i => $typeName) { if ($typeName === 'integer') { $suggestedName = 'int'; } elseif ($typeName === 'boolean') { $suggestedName = 'bool'; } elseif (in_array($typeName, array('int', 'bool'))) { $suggestedName = $typeName; } else { $suggestedName = PHP_CodeSniffer::suggestType($typeName); } if (in_array($suggestedName, $suggestedNames) === false) { $suggestedNames[] = $suggestedName; } } $suggestedType = implode('|', $suggestedNames); if ($content !== $suggestedType) { $error = 'Function return type "%s" is invalid'; $error = 'Expected "%s" but found "%s" for function return type'; $data = array( $suggestedType, $content, ); $phpcsFile->addError($error, $return, 'InvalidReturn', $data); } // If the return type is void, make sure there is // no return statement in the function. if ($content === 'void') { if (isset($tokens[$stackPtr]['scope_closer']) === true) { $endToken = $tokens[$stackPtr]['scope_closer']; for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) { if ($tokens[$returnToken]['code'] === T_CLOSURE) { $returnToken = $tokens[$returnToken]['scope_closer']; continue; } if ($tokens[$returnToken]['code'] === T_RETURN) { break; } } if ($returnToken !== $endToken) { // If the function is not returning anything, just // exiting, then there is no problem. $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); if ($tokens[$semicolon]['code'] !== T_SEMICOLON) { $error = 'Function return type is void, but function contains return statement'; $phpcsFile->addWarning($error, $return, 'InvalidReturnVoid'); } } }//end if } elseif (!preg_match('/^mixed/', $content)) { // If return type is not void, there needs to be a return statement // somewhere in the function that returns something. if (isset($tokens[$stackPtr]['scope_closer']) === true) { $endToken = $tokens[$stackPtr]['scope_closer']; $returnToken = $phpcsFile->findNext(T_RETURN, $stackPtr, $endToken); if ($returnToken === false) { $error = 'Function return type is not void, but function has no return statement'; $phpcsFile->addWarning($error, $return, 'InvalidNoReturn'); } elseif (!preg_match('/void/', $content)) { $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); if ($tokens[$semicolon]['code'] === T_SEMICOLON) { $error = 'Function return type is not void, but function is returning void here'; $phpcsFile->addWarning($error, $returnToken, 'InvalidReturnNotVoid'); } } } }//end if }//end if } else { $error = 'Missing @return tag in function comment'; $phpcsFile->addWarning($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn'); }//end if }//end processReturn() /** * Process any throw tags that this function comment has. * * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * @param int $commentStart The position in the stack where the comment started. * * @return void */ protected function processThrows(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) { $tokens = $phpcsFile->getTokens(); $throws = array(); foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { if ($tokens[$tag]['content'] !== '@throws') { continue; } $exception = null; $comment = null; if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { $matches = array(); preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[($tag + 2)]['content'], $matches); $exception = $matches[1]; if (isset($matches[2]) === true) { $comment = $matches[2]; } } if ($exception === null) { $error = 'Exception type and comment missing for @throws tag in function comment'; $phpcsFile->addWarning($error, $tag, 'InvalidThrows'); } elseif ($comment === null) { $error = 'Comment missing for @throws tag in function comment'; $phpcsFile->addWarning($error, $tag, 'EmptyThrows'); } else { // Any strings until the next tag belong to this comment. if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; } else { $end = $tokens[$commentStart]['comment_closer']; } for ($i = ($tag + 3); $i < $end; $i++) { if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { $comment .= ' '.$tokens[$i]['content']; } } // Starts with a capital letter and ends with a fullstop. $firstChar = $comment{0}; if (strtoupper($firstChar) !== $firstChar) { $error = '@throws tag comment must start with a capital letter'; $phpcsFile->addWarning($error, ($tag + 2), 'ThrowsNotCapital'); } $lastChar = substr($comment, -1); if ($lastChar !== '.') { $error = '@throws tag comment must end with a full stop'; $phpcsFile->addWarning($error, ($tag + 2), 'ThrowsNoFullStop'); } }//end if }//end foreach }//end processThrows() /** * Process the function parameter comments. * * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token * in the stack passed in $tokens. * @param int $commentStart The position in the stack where the comment started. * * @return void */ protected function processParams(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) { if ($this->isInheritDoc($phpcsFile, $stackPtr)) { return; } $tokens = $phpcsFile->getTokens(); $params = array(); $maxType = 0; $maxVar = 0; foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { if ($tokens[$tag]['content'] !== '@param') { continue; } $type = ''; $typeSpace = 0; $var = ''; $varSpace = 0; $comment = ''; $commentLines = array(); if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { $matches = array(); preg_match('/([^$&]+)(?:((?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches); $typeLen = strlen($matches[1]); $type = trim($matches[1]); $typeSpace = ($typeLen - strlen($type)); $typeLen = strlen($type); if ($typeLen > $maxType) { $maxType = $typeLen; } if (isset($matches[2]) === true) { $var = $matches[2]; $varLen = strlen($var); if ($varLen > $maxVar) { $maxVar = $varLen; } if (isset($matches[4]) === true) { $varSpace = strlen($matches[3]); $comment = $matches[4]; $commentLines[] = array( 'comment' => $comment, 'token' => ($tag + 2), 'indent' => $varSpace, ); // Any strings until the next tag belong to this comment. if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; } else { $end = $tokens[$commentStart]['comment_closer']; } for ($i = ($tag + 3); $i < $end; $i++) { if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { $indent = 0; if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) { $indent = strlen($tokens[($i - 1)]['content']); } $comment .= ' '.$tokens[$i]['content']; $commentLines[] = array( 'comment' => $tokens[$i]['content'], 'token' => $i, 'indent' => $indent, ); } } } else { $error = 'Missing parameter comment'; $phpcsFile->addError($error, $tag, 'MissingParamComment'); $commentLines[] = array('comment' => ''); }//end if } else { $error = 'Missing parameter name'; $phpcsFile->addError($error, $tag, 'MissingParamName'); }//end if } else { $error = 'Missing parameter type'; $phpcsFile->addError($error, $tag, 'MissingParamType'); }//end if $params[] = array( 'tag' => $tag, 'type' => $type, 'var' => $var, 'comment' => $comment, 'commentLines' => $commentLines, 'type_space' => $typeSpace, 'var_space' => $varSpace, ); }//end foreach $realParams = $phpcsFile->getMethodParameters($stackPtr); $foundParams = array(); foreach ($params as $pos => $param) { // If the type is empty, the whole line is empty. if ($param['type'] === '') { continue; } // Check the param type value. $typeNames = explode('|', $param['type']); foreach ($typeNames as $typeName) { if ($typeName === 'integer') { $suggestedName = 'int'; } elseif ($typeName === 'boolean') { $suggestedName = 'bool'; } elseif (in_array($typeName, array('int', 'bool'))) { $suggestedName = $typeName; } else { $suggestedName = PHP_CodeSniffer::suggestType($typeName); } if ($typeName !== $suggestedName) { $error = 'Expected "%s" but found "%s" for parameter type'; $data = array( $suggestedName, $typeName, ); $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data); if ($fix === true) { $content = $suggestedName; $content .= str_repeat(' ', $param['type_space']); $content .= $param['var']; $content .= str_repeat(' ', $param['var_space']); $content .= $param['commentLines'][0]['comment']; $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); } } }//end foreach if ($param['var'] === '') { continue; } $foundParams[] = $param['var']; // Make sure the param name is correct. if (isset($realParams[$pos]) === true) { $realName = $realParams[$pos]['name']; if ($realName !== $param['var']) { $code = 'ParamNameNoMatch'; $data = array( $param['var'], $realName, ); $error = 'Doc comment for parameter %s does not match '; if (strtolower($param['var']) === strtolower($realName)) { $error .= 'case of '; $code = 'ParamNameNoCaseMatch'; } $error .= 'actual variable name %s'; $phpcsFile->addWarning($error, $param['tag'], $code, $data); } } elseif (substr($param['var'], -4) !== ',...') { // We must have an extra parameter comment. $error = 'Superfluous parameter comment'; $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment'); }//end if if ($param['comment'] === '') { continue; } // Param comments must start with a capital letter and end with the full stop. $firstChar = $param['comment']{0}; if (preg_match('|\p{Lu}|u', $firstChar) === 0) { $error = 'Parameter comment must start with a capital letter'; $phpcsFile->addWarning($error, $param['tag'], 'ParamCommentNotCapital'); } $lastChar = substr($param['comment'], -1); if ($lastChar !== '.') { $error = 'Parameter comment must end with a full stop'; $phpcsFile->addWarning($error, $param['tag'], 'ParamCommentFullStop'); } }//end foreach $realNames = array(); foreach ($realParams as $realParam) { $realNames[] = $realParam['name']; } // Report missing comments. $diff = array_diff($realNames, $foundParams); foreach ($diff as $neededParam) { $error = 'Doc comment for parameter "%s" missing'; $data = array($neededParam); $phpcsFile->addWarning($error, $commentStart, 'MissingParamTag', $data); } }//end processParams() }//end class