You are here

protected function FunctionCommentSniff::processParams 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::processParams()
  2. 8.3.x coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php \Drupal\Sniffs\Commenting\FunctionCommentSniff::processParams()

Process the function parameter comments.

Parameters

\PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.:

int $stackPtr The position of the current token: in the stack passed in $tokens.

int $commentStart The position in the stack where the comment started.:

Return value

void

1 call to FunctionCommentSniff::processParams()
FunctionCommentSniff::process in coder_sniffer/Drupal/Sniffs/Commenting/FunctionCommentSniff.php
Processes this test, when one of its tokens is encountered.

File

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

Class

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

Namespace

Drupal\Sniffs\Commenting

Code

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'],
        ]);
      }
    }
  }
}