You are here

class FunctionCommentSniff in Coder 8.2

Same name and namespace in other branches
  1. 8.3 coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php \Drupal\Sniffs\Commenting\FunctionCommentSniff
  2. 8.3.x coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php \Drupal\Sniffs\Commenting\FunctionCommentSniff

Parses and verifies the doc comments for functions. Largely copied from PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\FunctionCommentSniff.

@category PHP @package PHP_CodeSniffer @link http://pear.php.net/package/PHP_CodeSniffer

Hierarchy

Expanded class hierarchy of FunctionCommentSniff

1 file declares its use of FunctionCommentSniff
VariableCommentSniff.php in coder_sniffer/Drupal/Sniffs/Commenting/VariableCommentSniff.php

File

coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php, line 24

Namespace

Drupal\Sniffs\Commenting
View source
class FunctionCommentSniff implements Sniff {

  /**
   * A map of invalid data types to valid ones for param and return documentation.
   *
   * @var array
   */
  public static $invalidTypes = array(
    'Array' => 'array',
    'array()' => 'array',
    '[]' => 'array',
    'boolean' => 'bool',
    'Boolean' => 'bool',
    'integer' => 'int',
    'str' => 'string',
    'stdClass' => 'object',
    'number' => 'int',
    'String' => 'string',
    'type' => 'mixed',
    'NULL' => 'null',
    'FALSE' => 'false',
    'TRUE' => 'true',
    'Bool' => 'bool',
    'Int' => 'int',
    'Integer' => 'int',
  );

  /**
   * An array of variable types for param/var we will check.
   *
   * @var array(string)
   */
  public $allowedTypes = array(
    'array',
    'mixed',
    'object',
    'resource',
    'callable',
  );

  /**
   * Returns an array of tokens this test wants to listen for.
   *
   * @return array
   */
  public function register() {
    return array(
      T_FUNCTION,
    );
  }

  //end register()

  /**
   * Processes this test, when one of its tokens is encountered.
   *
   * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
   * @param int                         $stackPtr  The position of the current token
   *                                               in the stack passed in $tokens.
   *
   * @return void
   */
  public function process(File $phpcsFile, $stackPtr) {
    $tokens = $phpcsFile
      ->getTokens();
    $find = Tokens::$methodPrefixes;
    $find[] = T_WHITESPACE;
    $commentEnd = $phpcsFile
      ->findPrevious($find, $stackPtr - 1, null, true);
    $beforeCommentEnd = $phpcsFile
      ->findPrevious(Tokens::$emptyTokens, $commentEnd - 1, null, true);
    if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG && $tokens[$commentEnd]['code'] !== T_COMMENT || $beforeCommentEnd !== false && $tokens[$beforeCommentEnd]['line'] === $tokens[$commentEnd]['line']) {
      $fix = $phpcsFile
        ->addFixableError('Missing function doc comment', $stackPtr, 'Missing');
      if ($fix === true) {
        $before = $phpcsFile
          ->findNext(T_WHITESPACE, $commentEnd + 1, $stackPtr + 1, true);
        $phpcsFile->fixer
          ->addContentBefore($before, "/**\n *\n */\n");
      }
      return;
    }
    if ($tokens[$commentEnd]['code'] === T_COMMENT) {
      $fix = $phpcsFile
        ->addFixableError('You must use "/**" style comments for a function comment', $stackPtr, 'WrongStyle');
      if ($fix === true) {

        // Convert the comment into a doc comment.
        $phpcsFile->fixer
          ->beginChangeset();
        $comment = '';
        for ($i = $commentEnd; $tokens[$i]['code'] === T_COMMENT; $i--) {
          $comment = ' *' . ltrim($tokens[$i]['content'], '/* ') . $comment;
          $phpcsFile->fixer
            ->replaceToken($i, '');
        }
        $phpcsFile->fixer
          ->replaceToken($commentEnd, "/**\n" . rtrim($comment, "*/\n") . "\n */\n");
        $phpcsFile->fixer
          ->endChangeset();
      }
      return;
    }
    $commentStart = $tokens[$commentEnd]['comment_opener'];
    foreach ($tokens[$commentStart]['comment_tags'] as $tag) {

      // This is a file comment, not a function comment.
      if ($tokens[$tag]['content'] === '@file') {
        $fix = $phpcsFile
          ->addFixableError('Missing function doc comment', $stackPtr, 'Missing');
        if ($fix === true) {
          $before = $phpcsFile
            ->findNext(T_WHITESPACE, $commentEnd + 1, $stackPtr + 1, true);
          $phpcsFile->fixer
            ->addContentBefore($before, "/**\n *\n */\n");
        }
        return;
      }
      if ($tokens[$tag]['content'] === '@see') {

        // Make sure the tag isn't empty.
        $string = $phpcsFile
          ->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
        if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
          $error = 'Content missing for @see tag in function comment';
          $phpcsFile
            ->addError($error, $tag, 'EmptySees');
        }
      }
    }

    //end foreach
    if ($tokens[$commentEnd]['line'] !== $tokens[$stackPtr]['line'] - 1) {
      $error = 'There must be no blank lines after the function comment';
      $fix = $phpcsFile
        ->addFixableError($error, $commentEnd, 'SpacingAfter');
      if ($fix === true) {
        $phpcsFile->fixer
          ->replaceToken($commentEnd + 1, '');
      }
    }
    $this
      ->processReturn($phpcsFile, $stackPtr, $commentStart);
    $this
      ->processThrows($phpcsFile, $stackPtr, $commentStart);
    $this
      ->processParams($phpcsFile, $stackPtr, $commentStart);
    $this
      ->processSees($phpcsFile, $stackPtr, $commentStart);
  }

  //end process()

  /**
   * Process the return comment of this function comment.
   *
   * @param \PHP_CodeSniffer\Files\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(File $phpcsFile, $stackPtr, $commentStart) {
    $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';
    $methodName = strtolower(ltrim($methodName, '_'));
    $return = null;
    foreach ($tokens[$commentStart]['comment_tags'] as $pos => $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;

        // 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'];
        }
      }
    }
    $type = null;
    if ($isSpecialMethod === false && $methodName !== $className) {
      if ($return !== null) {
        $type = trim($tokens[$return + 2]['content']);
        if (empty($type) === 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 {
          if (strpos($type, ' ') === false) {

            // Check return type (can be multiple, separated by '|').
            $typeNames = explode('|', $type);
            $suggestedNames = array();
            $hasNull = false;
            $hasMultiple = false;
            if (count($typeNames) > 0) {
              $hasMultiple = true;
            }
            foreach ($typeNames as $i => $typeName) {
              if (strtolower($typeName) === 'null') {
                $hasNull = true;
              }
              $suggestedName = static::suggestType($typeName);
              if (in_array($suggestedName, $suggestedNames) === false) {
                $suggestedNames[] = $suggestedName;
              }
            }
            $suggestedType = implode('|', $suggestedNames);
            if ($type !== $suggestedType) {
              $error = 'Expected "%s" but found "%s" for function return type';
              $data = array(
                $suggestedType,
                $type,
              );
              $fix = $phpcsFile
                ->addFixableError($error, $return, 'InvalidReturn', $data);
              if ($fix === true) {
                $content = $suggestedType;
                $phpcsFile->fixer
                  ->replaceToken($return + 2, $content);
              }
            }

            //end if
            if ($type === 'void') {
              $error = 'If there is no return value for a function, there must not be a @return tag.';
              $phpcsFile
                ->addError($error, $return, 'VoidReturn');
            }
            else {
              if ($type !== 'mixed') {

                // 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'];
                  $foundReturnToken = false;
                  $searchStart = $stackPtr;
                  $foundNonVoidReturn = false;
                  do {
                    $returnToken = $phpcsFile
                      ->findNext(T_RETURN, $searchStart, $endToken);
                    if ($returnToken === false && $foundReturnToken === false) {
                      $error = '@return doc comment specified, but function has no return statement';
                      $phpcsFile
                        ->addError($error, $return, 'InvalidNoReturn');
                    }
                    else {

                      // Check for return token as the last loop after the last return
                      // in the function will enter this else condition
                      // but without the returnToken.
                      if ($returnToken !== false) {
                        $foundReturnToken = true;
                        $semicolon = $phpcsFile
                          ->findNext(T_WHITESPACE, $returnToken + 1, null, true);
                        if ($tokens[$semicolon]['code'] === T_SEMICOLON) {

                          // Void return is allowed if the @return type has null in it.
                          if ($hasNull === false) {
                            $error = 'Function return type is not void, but function is returning void here';
                            $phpcsFile
                              ->addError($error, $returnToken, 'InvalidReturnNotVoid');
                          }
                        }
                        else {
                          $foundNonVoidReturn = true;
                        }

                        //end if
                        $searchStart = $returnToken + 1;
                      }

                      //end if
                    }

                    //end if
                  } while ($returnToken !== false);
                  if ($foundNonVoidReturn === false && $foundReturnToken === true) {
                    $error = 'Function return type is not void, but function does not have a non-void return statement';
                    $phpcsFile
                      ->addError($error, $return, 'InvalidReturnNotVoid');
                  }
                }

                //end if
              }
            }

            //end if
          }
        }

        //end if
        $comment = '';
        for ($i = $return + 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,
            );
            if ($indent < 3) {
              $error = 'Return comment indentation must be 3 spaces, found %s spaces';
              $fix = $phpcsFile
                ->addFixableError($error, $i, 'ReturnCommentIndentation', array(
                $indent,
              ));
              if ($fix === true) {
                $phpcsFile->fixer
                  ->replaceToken($i - 1, '   ');
              }
            }
          }
        }

        //end for

        // The first line of the comment must be indented no more than 3
        // spaces, the following lines can be more so we only check the first
        // line.
        if (empty($commentLines[0]['indent']) === false && $commentLines[0]['indent'] > 3) {
          $error = 'Return comment indentation must be 3 spaces, found %s spaces';
          $fix = $phpcsFile
            ->addFixableError($error, $commentLines[0]['token'] - 1, 'ReturnCommentIndentation', array(
            $commentLines[0]['indent'],
          ));
          if ($fix === true) {
            $phpcsFile->fixer
              ->replaceToken($commentLines[0]['token'] - 1, '   ');
          }
        }
        if ($comment === '' && $type !== '$this' && $type !== 'static') {
          if (strpos($type, ' ') !== false) {
            $error = 'Description for the @return value must be on the next line';
          }
          else {
            $error = 'Description for the @return value is missing';
          }
          $phpcsFile
            ->addError($error, $return, 'MissingReturnComment');
        }
        else {
          if (strpos($type, ' ') !== false) {
            if (preg_match('/^([^\\s]+)[\\s]+(\\$[^\\s]+)[\\s]*$/', $type, $matches) === 1) {
              $error = 'Return type must not contain variable name "%s"';
              $data = array(
                $matches[2],
              );
              $fix = $phpcsFile
                ->addFixableError($error, $return + 2, 'ReturnVarName', $data);
              if ($fix === true) {
                $phpcsFile->fixer
                  ->replaceToken($return + 2, $matches[1]);
              }
            }
            else {
              $error = 'Return type "%s" must not contain spaces';
              $data = array(
                $type,
              );
              $phpcsFile
                ->addError($error, $return, 'ReturnTypeSpaces', $data);
            }
          }
        }

        //end if
      }

      //end if
    }
    else {

      // No return tag for constructor and destructor.
      if ($return !== null) {
        $error = '@return tag is not required for constructor and destructor';
        $phpcsFile
          ->addError($error, $return, 'ReturnNotRequired');
      }
    }

    //end if
  }

  //end processReturn()

  /**
   * Process any throw tags that this function comment has.
   *
   * @param \PHP_CodeSniffer\Files\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(File $phpcsFile, $stackPtr, $commentStart) {
    $tokens = $phpcsFile
      ->getTokens();
    foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
      if ($tokens[$tag]['content'] !== '@throws') {
        continue;
      }
      if ($tokens[$tag + 2]['code'] !== T_DOC_COMMENT_STRING) {
        $error = 'Exception type missing for @throws tag in function comment';
        $phpcsFile
          ->addError($error, $tag, 'InvalidThrows');
      }
      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'];
        }
        $comment = '';
        $throwStart = null;
        for ($i = $tag + 3; $i < $end; $i++) {
          if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
            if ($throwStart === null) {
              $throwStart = $i;
            }
            $indent = 0;
            if ($tokens[$i - 1]['code'] === T_DOC_COMMENT_WHITESPACE) {
              $indent = strlen($tokens[$i - 1]['content']);
            }
            $comment .= ' ' . $tokens[$i]['content'];
            if ($indent < 3) {
              $error = 'Throws comment indentation must be 3 spaces, found %s spaces';
              $phpcsFile
                ->addError($error, $i, 'TrhowsCommentIndentation', array(
                $indent,
              ));
            }
          }
        }
        $comment = trim($comment);
        if ($comment === '') {
          if (str_word_count($tokens[$tag + 2]['content'], 0, '\\_') > 1) {
            $error = '@throws comment must be on the next line';
            $phpcsFile
              ->addError($error, $tag, 'ThrowsComment');
          }
          return;
        }

        // 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
            ->addError($error, $throwStart, 'ThrowsNotCapital');
        }
        $lastChar = substr($comment, -1);
        if (in_array($lastChar, array(
          '.',
          '!',
          '?',
        )) === false) {
          $error = '@throws tag comment must end with a full stop';
          $phpcsFile
            ->addError($error, $throwStart, 'ThrowsNoFullStop');
        }
      }

      //end if
    }

    //end foreach
  }

  //end processThrows()

  /**
   * Process the function parameter comments.
   *
   * @param \PHP_CodeSniffer\Files\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(File $phpcsFile, $stackPtr, $commentStart) {
    $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 there is more than one word then it is a comment that should be
        // on the next line.
        if (isset($matches[4]) === true && ($typeLen > 0 || preg_match('/[^\\s]+[\\s]+[^\\s]+/', $matches[4]) === 1)) {
          $comment = $matches[4];
          $error = 'Parameter comment must be on the next line';
          $fix = $phpcsFile
            ->addFixableError($error, $tag + 2, 'ParamCommentNewLine');
          if ($fix === true) {
            $parts = $matches;
            unset($parts[0]);
            $parts[3] = "\n *   ";
            $phpcsFile->fixer
              ->replaceToken($tag + 2, implode('', $parts));
          }
        }
        if (isset($matches[2]) === true) {
          $var = $matches[2];
        }
        else {
          $var = '';
        }
        if (substr($var, -1) === '.') {
          $error = 'Doc comment parameter name "%s" must not end with a dot';
          $fix = $phpcsFile
            ->addFixableError($error, $tag + 2, 'ParamNameDot', [
            $var,
          ]);
          if ($fix === true) {
            $content = $type . ' ' . substr($var, 0, -1);
            $phpcsFile->fixer
              ->replaceToken($tag + 2, $content);
          }

          // Continue with the next parameter to avoid confusing
          // overlapping errors further down.
          continue;
        }
        $varLen = strlen($var);
        if ($varLen > $maxVar) {
          $maxVar = $varLen;
        }

        // 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,
            );
            if ($indent < 3) {
              $error = 'Parameter comment indentation must be 3 spaces, found %s spaces';
              $fix = $phpcsFile
                ->addFixableError($error, $i, 'ParamCommentIndentation', array(
                $indent,
              ));
              if ($fix === true) {
                $phpcsFile->fixer
                  ->replaceToken($i - 1, '   ');
              }
            }
          }
        }

        //end for

        // The first line of the comment must be indented no more than 3
        // spaces, the following lines can be more so we only check the first
        // line.
        if (empty($commentLines[0]['indent']) === false && $commentLines[0]['indent'] > 3) {
          $error = 'Parameter comment indentation must be 3 spaces, found %s spaces';
          $fix = $phpcsFile
            ->addFixableError($error, $commentLines[0]['token'] - 1, 'ParamCommentIndentation', array(
            $commentLines[0]['indent'],
          ));
          if ($fix === true) {
            $phpcsFile->fixer
              ->replaceToken($commentLines[0]['token'] - 1, '   ');
          }
        }
        if ($comment === '') {
          $error = 'Missing parameter comment';
          $phpcsFile
            ->addError($error, $tag, 'MissingParamComment');
          $commentLines[] = array(
            'comment' => '',
          );
        }

        //end if
        $variableArguments = false;

        // Allow the "..." @param doc for a variable number of parameters.
        // This could happen with type defined as @param array ... or
        // without type defined as @param ...
        if ($tokens[$tag + 2]['content'] === '...' || substr($tokens[$tag + 2]['content'], -3) === '...' && count(explode(' ', $tokens[$tag + 2]['content'])) === 2) {
          $variableArguments = true;
        }
        if ($typeLen === 0) {
          $error = 'Missing parameter type';

          // If there is just one word as comment at the end of the line
          // then this is probably the data type. Move it before the
          // variable name.
          if (isset($matches[4]) === true && preg_match('/[^\\s]+[\\s]+[^\\s]+/', $matches[4]) === 0) {
            $fix = $phpcsFile
              ->addFixableError($error, $tag, 'MissingParamType');
            if ($fix === true) {
              $phpcsFile->fixer
                ->replaceToken($tag + 2, $matches[4] . ' ' . $var);
            }
          }
          else {
            $phpcsFile
              ->addError($error, $tag, 'MissingParamType');
          }
        }
        if (empty($matches[2]) === true && $variableArguments === false) {
          $error = 'Missing parameter name';
          $phpcsFile
            ->addError($error, $tag, 'MissingParamName');
        }
      }
      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();
    $checkPos = 0;
    foreach ($params as $pos => $param) {
      if ($param['var'] === '') {
        continue;
      }
      $foundParams[] = $param['var'];

      // If the type is empty, the whole line is empty.
      if ($param['type'] === '') {
        continue;
      }

      // Make sure the param name is correct.
      $matched = false;

      // Parameter documentation can be omitted for some parameters, so we have
      // to search the rest for a match.
      $realName = '<undefined>';
      while (isset($realParams[$checkPos]) === true) {
        $realName = $realParams[$checkPos]['name'];
        if ($realName === $param['var'] || $realParams[$checkPos]['pass_by_reference'] === true && '&' . $realName === $param['var']) {
          $matched = true;
          break;
        }
        $checkPos++;
      }

      // Check the param type value. This could also be multiple parameter
      // types separated by '|'.
      $typeNames = explode('|', $param['type']);
      $suggestedNames = array();
      foreach ($typeNames as $i => $typeName) {
        $suggestedNames[] = static::suggestType($typeName);
      }
      $suggestedType = implode('|', $suggestedNames);
      if (preg_match('/\\s/', $param['type']) === 1) {
        $error = 'Parameter type "%s" must not contain spaces';
        $data = array(
          $param['type'],
        );
        $phpcsFile
          ->addError($error, $param['tag'], 'ParamTypeSpaces', $data);
      }
      else {
        if ($param['type'] !== $suggestedType) {
          $error = 'Expected "%s" but found "%s" for parameter type';
          $data = array(
            $suggestedType,
            $param['type'],
          );
          $fix = $phpcsFile
            ->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data);
          if ($fix === true) {
            $content = $suggestedType;
            $content .= str_repeat(' ', $param['type_space']);
            $content .= $param['var'];
            $phpcsFile->fixer
              ->replaceToken($param['tag'] + 2, $content);
          }
        }
      }
      if (count($typeNames) === 1) {
        $typeName = $param['type'];
        $suggestedName = static::suggestType($typeName);
      }

      // This runs only if there is only one type name and the type name
      // is not one of the disallowed type names.
      if (count($typeNames) === 1 && $typeName === $suggestedName) {

        // Check type hint for array and custom type.
        $suggestedTypeHint = '';
        if (strpos($suggestedName, 'array') !== false) {
          $suggestedTypeHint = 'array';
        }
        else {
          if (strpos($suggestedName, 'callable') !== false) {
            $suggestedTypeHint = 'callable';
          }
          else {
            if (substr($suggestedName, -2) === '[]') {
              $suggestedTypeHint = 'array';
            }
            else {
              if ($suggestedName === 'object') {
                $suggestedTypeHint = '';
              }
              else {
                if (in_array($typeName, $this->allowedTypes) === false) {
                  $suggestedTypeHint = $suggestedName;
                }
              }
            }
          }
        }
        if ($suggestedTypeHint !== '' && isset($realParams[$checkPos]) === true) {
          $typeHint = $realParams[$checkPos]['type_hint'];

          // Primitive type hints are allowed to be omitted.
          if ($typeHint === '' && in_array($suggestedTypeHint, [
            'string',
            'int',
            'float',
            'bool',
          ]) === false) {
            $error = 'Type hint "%s" missing for %s';
            $data = array(
              $suggestedTypeHint,
              $param['var'],
            );
            $phpcsFile
              ->addError($error, $stackPtr, 'TypeHintMissing', $data);
          }
          else {
            if ($typeHint !== $suggestedTypeHint && $typeHint !== '') {

              // The type hint could be fully namespaced, so we check
              // for the part after the last "\".
              $name_parts = explode('\\', $suggestedTypeHint);
              $last_part = end($name_parts);
              if ($last_part !== $typeHint && $this
                ->isAliasedType($typeHint, $suggestedTypeHint, $phpcsFile) === false) {
                $error = 'Expected type hint "%s"; found "%s" for %s';
                $data = array(
                  $last_part,
                  $typeHint,
                  $param['var'],
                );
                $phpcsFile
                  ->addError($error, $stackPtr, 'IncorrectTypeHint', $data);
              }
            }
          }

          //end if
        }
        else {
          if ($suggestedTypeHint === '' && isset($realParams[$checkPos]) === true) {
            $typeHint = $realParams[$checkPos]['type_hint'];
            if ($typeHint !== '' && $typeHint !== 'stdClass') {
              $error = 'Unknown type hint "%s" found for %s';
              $data = array(
                $typeHint,
                $param['var'],
              );
              $phpcsFile
                ->addError($error, $stackPtr, 'InvalidTypeHint', $data);
            }
          }
        }

        //end if
      }

      //end if

      // Check number of spaces after the type.
      $spaces = 1;
      if ($param['type_space'] !== $spaces) {
        $error = 'Expected %s spaces after parameter type; %s found';
        $data = array(
          $spaces,
          $param['type_space'],
        );
        $fix = $phpcsFile
          ->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data);
        if ($fix === true) {
          $phpcsFile->fixer
            ->beginChangeset();
          $content = $param['type'];
          $content .= str_repeat(' ', $spaces);
          $content .= $param['var'];
          $content .= str_repeat(' ', $param['var_space']);

          // At this point there is no description expected in the
          // @param line so no need to append comment.
          $phpcsFile->fixer
            ->replaceToken($param['tag'] + 2, $content);

          // Fix up the indent of additional comment lines.
          foreach ($param['commentLines'] as $lineNum => $line) {
            if ($lineNum === 0 || $param['commentLines'][$lineNum]['indent'] === 0) {
              continue;
            }
            $newIndent = $param['commentLines'][$lineNum]['indent'] + $spaces - $param['type_space'];
            $phpcsFile->fixer
              ->replaceToken($param['commentLines'][$lineNum]['token'] - 1, str_repeat(' ', $newIndent));
          }
          $phpcsFile->fixer
            ->endChangeset();
        }

        //end if
      }

      //end if
      if ($matched === false) {
        if ($checkPos >= $pos) {
          $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
            ->addError($error, $param['tag'], $code, $data);

          // Reset the parameter position to check for following
          // parameters.
          $checkPos = $pos - 1;
        }
        else {
          if (substr($param['var'], -4) !== ',...') {

            // We must have an extra parameter comment.
            $error = 'Superfluous parameter comment';
            $phpcsFile
              ->addError($error, $param['tag'], 'ExtraParamComment');
          }
        }

        //end if
      }

      //end if
      $checkPos++;
      if ($param['comment'] === '') {
        continue;
      }

      // Param comments must start with a capital letter and end with the full stop.
      if (isset($param['commentLines'][0]['comment']) === true) {
        $firstChar = $param['commentLines'][0]['comment'];
      }
      else {
        $firstChar = $param['comment'];
      }
      if (preg_match('|\\p{Lu}|u', $firstChar) === 0) {
        $error = 'Parameter comment must start with a capital letter';
        if (isset($param['commentLines'][0]['token']) === true) {
          $commentToken = $param['commentLines'][0]['token'];
        }
        else {
          $commentToken = $param['tag'];
        }
        $phpcsFile
          ->addError($error, $commentToken, 'ParamCommentNotCapital');
      }
      $lastChar = substr($param['comment'], -1);
      if (in_array($lastChar, array(
        '.',
        '!',
        '?',
        ')',
      )) === false) {
        $error = 'Parameter comment must end with a full stop';
        if (empty($param['commentLines']) === true) {
          $commentToken = $param['tag'] + 2;
        }
        else {
          $lastLine = end($param['commentLines']);
          $commentToken = $lastLine['token'];
        }
        $fix = $phpcsFile
          ->addFixableError($error, $commentToken, 'ParamCommentFullStop');
        if ($fix === true) {

          // Add a full stop as the last character of the comment.
          $phpcsFile->fixer
            ->addContent($commentToken, '.');
        }
      }
    }

    //end foreach

    // Missing parameters only apply to methods and not function because on
    // functions it is allowed to leave out param comments for form constructors
    // for example.
    // It is also allowed to ommit pram tags completely, in which case we don't
    // throw errors. Only throw errors if param comments exists but are
    // incomplete on class methods.
    if ($tokens[$stackPtr]['level'] > 0 && empty($foundParams) === false) {
      foreach ($realParams as $realParam) {
        $realParamKeyName = $realParam['name'];
        if (in_array($realParamKeyName, $foundParams) === false && ($realParam['pass_by_reference'] === true && in_array("&{$realParamKeyName}", $foundParams) === true) === false) {
          $error = 'Parameter %s is not described in comment';
          $phpcsFile
            ->addError($error, $commentStart, 'ParamMissingDefinition', [
            $realParam['name'],
          ]);
        }
      }
    }
  }

  //end processParams()

  /**
   * Process the function "see" comments.
   *
   * @param \PHP_CodeSniffer\Files\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 processSees(File $phpcsFile, $stackPtr, $commentStart) {
    $tokens = $phpcsFile
      ->getTokens();
    foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
      if ($tokens[$tag]['content'] !== '@see') {
        continue;
      }
      if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING) {
        $comment = $tokens[$tag + 2]['content'];
        if (strpos($comment, ' ') !== false) {
          $error = 'The @see reference should not contain any additional text';
          $phpcsFile
            ->addError($error, $tag, 'SeeAdditionalText');
          continue;
        }
        if (preg_match('/[\\.!\\?]$/', $comment) === 1) {
          $error = 'Trailing punctuation for @see references is not allowed.';
          $fix = $phpcsFile
            ->addFixableError($error, $tag, 'SeePunctuation');
          if ($fix === true) {

            // Replace the last character from the comment which is
            // already tested to be a punctuation.
            $content = substr($comment, 0, -1);
            $phpcsFile->fixer
              ->replaceToken($tag + 2, $content);
          }

          //end if
        }
      }
    }

    //end foreach
  }

  //end processSees()

  /**
   * Returns a valid variable type for param/var tag.
   *
   * @param string $type The variable type to process.
   *
   * @return string
   */
  public static function suggestType($type) {
    if (isset(static::$invalidTypes[$type]) === true) {
      return static::$invalidTypes[$type];
    }
    if ($type === '$this') {
      return $type;
    }
    $type = preg_replace('/[^a-zA-Z0-9_\\\\[\\]]/', '', $type);
    return $type;
  }

  //end suggestType()

  /**
   * Checks if a used type hint is an alias defined by a "use" statement.
   *
   * @param string                      $typeHint          The type hint used.
   * @param string                      $suggestedTypeHint The fully qualified type to
   *                                                       check against.
   * @param \PHP_CodeSniffer\Files\File $phpcsFile         The file being checked.
   *
   * @return boolean
   */
  protected function isAliasedType($typeHint, $suggestedTypeHint, File $phpcsFile) {
    $tokens = $phpcsFile
      ->getTokens();

    // Iterate over all "use" statements in the file.
    $usePtr = 0;
    while ($usePtr !== false) {
      $usePtr = $phpcsFile
        ->findNext(T_USE, $usePtr + 1);
      if ($usePtr === false) {
        return false;
      }

      // Only check use statements in the global scope.
      if (empty($tokens[$usePtr]['conditions']) === false) {
        continue;
      }

      // Now comes the original class name, possibly with namespace
      // backslashes.
      $originalClass = $phpcsFile
        ->findNext(Tokens::$emptyTokens, $usePtr + 1, null, true);
      if ($originalClass === false || $tokens[$originalClass]['code'] !== T_STRING && $tokens[$originalClass]['code'] !== T_NS_SEPARATOR) {
        continue;
      }
      $originalClassName = '';
      while (in_array($tokens[$originalClass]['code'], array(
        T_STRING,
        T_NS_SEPARATOR,
      )) === true) {
        $originalClassName .= $tokens[$originalClass]['content'];
        $originalClass++;
      }
      if (ltrim($originalClassName, '\\') !== ltrim($suggestedTypeHint, '\\')) {
        continue;
      }

      // Now comes the "as" keyword signaling an alias name for the class.
      $asPtr = $phpcsFile
        ->findNext(Tokens::$emptyTokens, $originalClass + 1, null, true);
      if ($asPtr === false || $tokens[$asPtr]['code'] !== T_AS) {
        continue;
      }

      // Now comes the name the class is aliased to.
      $aliasPtr = $phpcsFile
        ->findNext(Tokens::$emptyTokens, $asPtr + 1, null, true);
      if ($aliasPtr === false || $tokens[$aliasPtr]['code'] !== T_STRING || $tokens[$aliasPtr]['content'] !== $typeHint) {
        continue;
      }

      // We found a use statement that aliases the used type hint!
      return true;
    }

    //end while
    return false;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
FunctionCommentSniff::$allowedTypes public property An array of variable types for param/var we will check.
FunctionCommentSniff::$invalidTypes public static property A map of invalid data types to valid ones for param and return documentation.
FunctionCommentSniff::isAliasedType protected function Checks if a used type hint is an alias defined by a "use" statement.
FunctionCommentSniff::process public function Processes this test, when one of its tokens is encountered.
FunctionCommentSniff::processParams protected function Process the function parameter comments.
FunctionCommentSniff::processReturn protected function Process the return comment of this function comment.
FunctionCommentSniff::processSees protected function Process the function "see" comments.
FunctionCommentSniff::processThrows protected function Process any throw tags that this function comment has.
FunctionCommentSniff::register public function Returns an array of tokens this test wants to listen for.
FunctionCommentSniff::suggestType public static function Returns a valid variable type for param/var tag.