View source
<?php
class Drupal_Sniffs_Commenting_FunctionCommentSniff implements PHP_CodeSniffer_Sniff {
private $_methodName = '';
private $_functionToken = null;
private $_classToken = null;
protected $commentParser = null;
protected $currentFile = null;
protected $invalidTypes = array(
'Array' => 'array',
'boolean' => 'bool',
'Boolean' => 'bool',
'integer' => 'int',
'str' => 'string',
'stdClass' => 'object',
'number' => 'int',
'String' => 'string',
);
public function register() {
return array(
T_FUNCTION,
);
}
public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) {
$this->currentFile = $phpcsFile;
$tokens = $phpcsFile
->getTokens();
$find = array(
T_COMMENT,
T_DOC_COMMENT,
T_CLASS,
T_FUNCTION,
T_OPEN_TAG,
);
$commentEnd = $phpcsFile
->findPrevious($find, $stackPtr - 1);
if ($commentEnd === false) {
return;
}
$code = $tokens[$commentEnd]['code'];
if ($code === T_COMMENT) {
$prevContent = $phpcsFile
->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, $commentEnd);
if ($tokens[$commentEnd]['line'] === $tokens[$commentEnd]['line']) {
$error = 'Missing function doc comment';
$phpcsFile
->addError($error, $stackPtr, 'Missing');
}
else {
$error = 'You must use "/**" style comments for a function comment';
$phpcsFile
->addError($error, $stackPtr, 'WrongStyle');
}
return;
}
else {
if ($code !== T_DOC_COMMENT) {
$error = 'Missing function doc comment';
$phpcsFile
->addError($error, $stackPtr, 'Missing');
return;
}
else {
if (trim($tokens[$commentEnd]['content']) !== '*/') {
$error = 'Wrong function doc comment end; expected "*/", found "%s"';
$phpcsFile
->addError($error, $commentEnd, 'WrongEnd', array(
trim($tokens[$commentEnd]['content']),
));
return;
}
}
}
$ignore = PHP_CodeSniffer_Tokens::$scopeModifiers;
$ignore[] = T_STATIC;
$ignore[] = T_WHITESPACE;
$ignore[] = T_ABSTRACT;
$ignore[] = T_FINAL;
$prevToken = $phpcsFile
->findPrevious($ignore, $stackPtr - 1, null, true);
if ($prevToken !== $commentEnd) {
$phpcsFile
->addError('Missing function doc comment', $stackPtr, 'Missing');
return;
}
$this->_functionToken = $stackPtr;
$this->_classToken = null;
foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) {
if ($condition === T_CLASS || $condition === T_INTERFACE) {
$this->_classToken = $condPtr;
break;
}
}
$commentStart = $phpcsFile
->findPrevious(T_DOC_COMMENT, $commentEnd - 1, null, true) + 1;
$prevToken = $phpcsFile
->findPrevious(T_WHITESPACE, $commentStart - 1, null, true);
if ($tokens[$prevToken]['code'] === T_OPEN_TAG) {
if (($stackPtr === 0 || $phpcsFile
->findPrevious(T_OPEN_TAG, $prevToken - 1) === false) && $tokens[$commentEnd]['line'] + 1 !== $tokens[$stackPtr]['line']) {
$phpcsFile
->addError('Missing function doc comment', $stackPtr, 'Missing');
return;
}
}
$commentString = $phpcsFile
->getTokensAsString($commentStart, $commentEnd - $commentStart + 1);
$this->_methodName = $phpcsFile
->getDeclarationName($stackPtr);
try {
$this->commentParser = new Drupal_CommentParser_FunctionCommentParser($commentString, $phpcsFile);
$this->commentParser
->parse();
} catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
$line = $e
->getLineWithinComment() + $commentStart;
$phpcsFile
->addError($e
->getMessage(), $line, 'FailedParse');
return;
}
$comment = $this->commentParser
->getComment();
if (is_null($comment) === true) {
$error = 'Function doc comment is empty';
$phpcsFile
->addError($error, $commentStart, 'Empty');
return;
}
$eolPos = strpos($commentString, $phpcsFile->eolChar);
$firstLine = substr($commentString, 0, $eolPos);
if ($firstLine !== '/**') {
$error = 'The open comment tag must be the only content on the line';
$phpcsFile
->addError($error, $commentStart, 'ContentAfterOpen');
}
if ($tokens[$commentEnd]['line'] + 1 !== $tokens[$stackPtr]['line']) {
$error = 'Function doc comment must end on the line before the function definition';
$phpcsFile
->addError($error, $commentEnd, 'EmptyLinesAfterDoc');
}
$this
->processParams($commentStart);
$this
->processReturn($commentStart, $commentEnd);
$this
->processThrows($commentStart);
$this
->processSees($commentStart);
if (preg_match('/^[\\s]*Implement[^\\n]+?hook_[^\\n]+/i', $comment
->getShortComment(), $matches)) {
if (!strstr($matches[0], 'Implements ') || strstr($matches[0], 'Implements of') || !preg_match('/ (drush_)?hook_[a-zA-Z0-9_]+\\(\\)( for [a-z0-9_-]+(\\(\\)|\\.tpl\\.php|\\.html.twig))?\\.$/', $matches[0])) {
$phpcsFile
->addWarning('Format should be "* Implements hook_foo().", "* Implements hook_foo_BAR_ID_bar() for xyz_bar().",, "* Implements hook_foo_BAR_ID_bar() for xyz-bar.html.twig.", or "* Implements hook_foo_BAR_ID_bar() for xyz-bar.tpl.php.".', $commentStart + 1);
}
else {
$params = $this->commentParser
->getParams();
if (empty($params) === false) {
$param = array_shift($params);
$errorPos = $param
->getLine() + $commentStart;
$warn = 'Hook implementations should not duplicate @param documentation';
$phpcsFile
->addWarning($warn, $errorPos, 'HookParamDoc');
}
$return = $this->commentParser
->getReturn();
if ($return !== null) {
$errorPos = $commentStart + $this->commentParser
->getReturn()
->getLine();
$warn = 'Hook implementations should not duplicate @return documentation';
$phpcsFile
->addWarning($warn, $errorPos, 'HookReturnDoc');
}
}
}
$short = $comment
->getShortComment();
if (trim($short) === '') {
$error = 'Missing short description in function doc comment';
$phpcsFile
->addError($error, $commentStart, 'MissingShort');
return;
}
$newlineCount = 0;
$newlineSpan = strspn($short, $phpcsFile->eolChar);
if ($short !== '' && $newlineSpan > 0) {
$line = $newlineSpan > 1 ? 'newlines' : 'newline';
$error = "Extra {$line} found before function comment short description";
$phpcsFile
->addError($error, $commentStart + 1);
return;
}
$newlineCount = substr_count($short, $phpcsFile->eolChar) + 1;
$long = $comment
->getLongComment();
if (empty($long) === false) {
$between = $comment
->getWhiteSpaceBetween();
$newlineBetween = substr_count($between, $phpcsFile->eolChar);
if ($newlineBetween !== 2) {
$error = 'There must be exactly one blank line between descriptions in function comment';
$phpcsFile
->addError($error, $commentStart + $newlineCount + 1, 'SpacingAfterShort');
}
$newlineCount += $newlineBetween;
}
$testShort = trim($short);
$lastChar = $testShort[strlen($testShort) - 1];
if (substr_count($testShort, $phpcsFile->eolChar) !== 0) {
$error = 'Function comment short description must be on a single line, further text should be a separate paragraph';
$phpcsFile
->addError($error, $commentStart + 1, 'ShortSingleLine');
}
if (strpos($short, $testShort) !== 1) {
$error = 'Function comment short description must start with exactly one space';
$phpcsFile
->addError($error, $commentStart + 1, 'ShortStartSpace');
}
if ($testShort !== '{@inheritdoc}') {
if (preg_match('|[A-Z]|', $testShort[0]) === 0) {
$error = 'Function comment short description must start with a capital letter';
$phpcsFile
->addError($error, $commentStart + 1, 'ShortNotCapital');
}
if ($lastChar !== '.') {
$error = 'Function comment short description must end with a full stop';
$phpcsFile
->addError($error, $commentStart + 1, 'ShortFullStop');
}
}
}
protected function processThrows($commentStart) {
if (count($this->commentParser
->getThrows()) === 0) {
return;
}
foreach ($this->commentParser
->getThrows() as $throw) {
$exception = $throw
->getValue();
$errorPos = $commentStart + $throw
->getLine();
if ($exception === '') {
$error = '@throws tag must contain the exception class name';
$this->currentFile
->addError($error, $errorPos, 'EmptyThrows');
}
}
}
protected function processReturn($commentStart, $commentEnd) {
$className = '';
if ($this->_classToken !== null) {
$className = $this->currentFile
->getDeclarationName($this->_classToken);
$className = strtolower(ltrim($className, '_'));
}
$methodName = strtolower(ltrim($this->_methodName, '_'));
$isSpecialMethod = $this->_methodName === '__construct' || $this->_methodName === '__destruct';
if ($isSpecialMethod === false && $methodName !== $className) {
$return = $this->commentParser
->getReturn();
if ($return !== null) {
$errorPos = $commentStart + $this->commentParser
->getReturn()
->getLine();
if (trim($return
->getRawContent()) === '') {
$error = '@return tag is empty in function comment';
$this->currentFile
->addError($error, $errorPos, 'EmptyReturn');
return;
}
$comment = $return
->getComment();
$commentWhitespace = $return
->getWhitespaceBeforeComment();
if (substr_count($return
->getWhitespaceBeforeValue(), $this->currentFile->eolChar) > 0) {
$error = 'Data type of return value is missing';
$this->currentFile
->addError($error, $errorPos, 'MissingReturnType');
$comment = $return
->getValue() . ' ' . $comment;
$commentWhitespace = $return
->getWhitespaceBeforeValue();
}
else {
if ($return
->getWhitespaceBeforeValue() !== ' ') {
$error = 'Expected 1 space before return type';
$this->currentFile
->addError($error, $errorPos, 'SpacingBeforeReturnType');
}
}
if (strpos($return
->getValue(), '$') !== false && $return
->getValue() !== '$this') {
$error = '@return data type must not contain "$"';
$this->currentFile
->addError($error, $errorPos, '$InReturnType');
}
if (in_array($return
->getValue(), array(
'unknown_type',
'<type>',
'type',
)) === true) {
$error = 'Expected a valid @return data type, but found %s';
$data = array(
$return
->getValue(),
);
$this->currentFile
->addError($error, $errorPos, 'InvalidReturnType', $data);
}
if (strtolower($return
->getValue()) === 'void') {
$error = 'If there is no return value for a function, there must not be a @return tag.';
$this->currentFile
->addError($error, $errorPos, 'VoidReturn');
}
if (isset($this->invalidTypes[$return
->getValue()]) === true) {
$error = 'Invalid @return data type, expected %s but found %s';
$data = array(
$this->invalidTypes[$return
->getValue()],
$return
->getValue(),
);
$this->currentFile
->addError($error, $errorPos, 'InvalidReturnTypeName', $data);
}
if (trim($comment) === '') {
$error = 'Missing comment for @return statement';
$this->currentFile
->addError($error, $errorPos, 'MissingReturnComment');
}
else {
if (substr_count($commentWhitespace, $this->currentFile->eolChar) !== 1) {
$error = 'Return comment must be on the next line';
$this->currentFile
->addError($error, $errorPos, 'ReturnCommentNewLine');
}
else {
if (substr_count($commentWhitespace, ' ') !== 3) {
$error = 'Return comment indentation must be 2 additional spaces';
$this->currentFile
->addError($error, $errorPos + 1, 'ParamCommentIndentation');
}
}
}
}
}
}
protected function processParams($commentStart) {
$realParams = $this->currentFile
->getMethodParameters($this->_functionToken);
$params = $this->commentParser
->getParams();
$foundParams = array();
if (empty($params) === false) {
if (substr_count($params[0]
->getWhitespaceBefore(), $this->currentFile->eolChar) < 2) {
$error = 'There must be an empty line before the parameter block';
$errorPos = $params[0]
->getLine() + $commentStart;
$this->currentFile
->addError($error, $errorPos, 'SpacingBeforeParams');
}
$lastParm = count($params) - 1;
if (substr_count($params[$lastParm]
->getWhitespaceAfter(), $this->currentFile->eolChar) !== 2) {
$error = 'Last parameter comment requires a blank newline after it';
$errorPos = $params[$lastParm]
->getLine() + $commentStart;
$this->currentFile
->addError($error, $errorPos, 'SpacingAfterParams');
}
$checkPos = 0;
foreach ($params as $param) {
$paramComment = trim($param
->getComment());
$errorPos = $param
->getLine() + $commentStart;
if ($param
->getWhitespaceBeforeType() !== ' ') {
$error = 'Expected 1 space before variable type';
$this->currentFile
->addError($error, $errorPos, 'SpacingBeforeParamType');
}
$pos = $param
->getPosition();
$paramName = '[ UNKNOWN ]';
if ($param
->getVarName() !== '') {
$paramName = $param
->getVarName();
}
$matched = false;
while (isset($realParams[$checkPos]) === true) {
$realName = $realParams[$checkPos]['name'];
$expectedParamName = $realName;
$isReference = $realParams[$checkPos]['pass_by_reference'];
if ($isReference === true && substr($paramName, 0, 1) === '&') {
$expectedParamName = '&' . $realName;
}
if ($expectedParamName === $paramName) {
$matched = true;
break;
}
$checkPos++;
}
if ($matched === false && $paramName !== '...') {
if ($checkPos >= $pos) {
$code = 'ParamNameNoMatch';
$data = array(
$paramName,
$realParams[$pos - 1]['name'],
$pos,
);
$error = 'Doc comment for var %s does not match ';
if (strtolower($paramName) === strtolower($realParams[$pos - 1]['name'])) {
$error .= 'case of ';
$code = 'ParamNameNoCaseMatch';
}
$error .= 'actual variable name %s at position %s';
$this->currentFile
->addError($error, $errorPos, $code, $data);
$checkPos = $pos - 1;
}
else {
$error = 'Superfluous doc comment at position ' . $pos;
$this->currentFile
->addError($error, $errorPos, 'ExtraParamComment');
}
}
$checkPos++;
if ($param
->getVarName() === '') {
$error = 'Missing parameter name at position ' . $pos;
$this->currentFile
->addError($error, $errorPos, 'MissingParamName');
}
if ($param
->getType() === '') {
$error = 'Missing parameter type at position ' . $pos;
$this->currentFile
->addError($error, $errorPos, 'MissingParamType');
}
if (in_array($param
->getType(), array(
'unknown_type',
'<type>',
'type',
)) === true) {
$error = 'Expected a valid @param data type, but found %s';
$data = array(
$param
->getType(),
);
$this->currentFile
->addError($error, $errorPos, 'InvalidParamType', $data);
}
if (isset($this->invalidTypes[$param
->getType()]) === true) {
$error = 'Invalid @param data type, expected %s but found %s';
$data = array(
$this->invalidTypes[$param
->getType()],
$param
->getType(),
);
$this->currentFile
->addError($error, $errorPos, 'InvalidParamTypeName', $data);
}
if ($paramComment === '') {
$error = 'Missing comment for param "%s" at position %s';
$data = array(
$paramName,
$pos,
);
$this->currentFile
->addError($error, $errorPos, 'MissingParamComment', $data);
}
else {
if (substr_count($param
->getWhitespaceBeforeComment(), $this->currentFile->eolChar) !== 1) {
$error = 'Parameter comment must be on the next line at position ' . $pos;
$this->currentFile
->addError($error, $errorPos, 'ParamCommentNewLine');
}
else {
if (substr_count($param
->getWhitespaceBeforeComment(), ' ') !== 3) {
$error = 'Parameter comment indentation must be 2 additional spaces at position ' . $pos;
$this->currentFile
->addError($error, $errorPos + 1, 'ParamCommentIndentation');
}
}
}
}
}
$realNames = array();
foreach ($realParams as $realParam) {
$realNames[] = $realParam['name'];
}
}
protected function processSees($commentStart) {
$sees = $this->commentParser
->getSees();
foreach ($sees as $see) {
$errorPos = $see
->getLine() + $commentStart;
if ($see
->getWhitespaceBeforeContent() !== ' ') {
$error = 'Expected 1 space before see reference';
$this->currentFile
->addError($error, $errorPos, 'SpacingBeforeSee');
}
$comment = trim($see
->getContent());
if (strpos($comment, ' ') !== false) {
$error = 'The @see reference should not contain any additional text';
$this->currentFile
->addError($error, $errorPos, 'SeeAdditionalText');
continue;
}
if (preg_match('/[\\.!\\?]$/', $comment) === 1) {
$error = 'Trailing punctuation for @see references is not allowed.';
$this->currentFile
->addError($error, $errorPos, 'SeePunctuation');
}
}
}
}