PHP_CodeSniffer
[ class tree: PHP_CodeSniffer ] [ index: PHP_CodeSniffer ] [ all elements ]

Source for file FunctionCommentSniff.php

Documentation is available at FunctionCommentSniff.php

  1. <?php
  2. /**
  3.  * Parses and verifies the doc comments for functions.
  4.  *
  5.  * @author    Greg Sherwood <gsherwood@squiz.net>
  6.  * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
  7.  * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
  8.  */
  9.  
  10. namespace PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting;
  11.  
  12. use PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FunctionCommentSniff as PEARFunctionCommentSniff;
  13. use PHP_CodeSniffer\Files\File;
  14. use PHP_CodeSniffer\Util\Common;
  15.  
  16. class FunctionCommentSniff extends PEARFunctionCommentSniff
  17. {
  18.  
  19.  
  20.     /**
  21.      * Process the return comment of this function comment.
  22.      *
  23.      * @param PHP_CodeSniffer_File $phpcsFile    The file being scanned.
  24.      * @param int                  $stackPtr     The position of the current token
  25.      *                                            in the stack passed in $tokens.
  26.      * @param int                  $commentStart The position in the stack where the comment started.
  27.      *
  28.      * @return void 
  29.      */
  30.     protected function processReturn(File $phpcsFile$stackPtr$commentStart)
  31.     {
  32.         $tokens $phpcsFile->getTokens();
  33.  
  34.         // Skip constructor and destructor.
  35.         $methodName      $phpcsFile->getDeclarationName($stackPtr);
  36.         $isSpecialMethod ($methodName === '__construct' || $methodName === '__destruct');
  37.  
  38.         $return = null;
  39.         foreach ($tokens[$commentStart]['comment_tags'as $tag{
  40.             if ($tokens[$tag]['content'=== '@return'{
  41.                 if ($return !== null{
  42.                     $error 'Only 1 @return tag is allowed in a function comment';
  43.                     $phpcsFile->addError($error$tag'DuplicateReturn');
  44.                     return;
  45.                 }
  46.  
  47.                 $return $tag;
  48.             }
  49.         }
  50.  
  51.         if ($isSpecialMethod === true{
  52.             return;
  53.         }
  54.  
  55.         if ($return !== null{
  56.             $content $tokens[($return + 2)]['content'];
  57.             if (empty($content=== true || $tokens[($return + 2)]['code'!== T_DOC_COMMENT_STRING{
  58.                 $error 'Return type missing for @return tag in function comment';
  59.                 $phpcsFile->addError($error$return'MissingReturnType');
  60.             else {
  61.                 // Check return type (can be multiple, separated by '|').
  62.                 $typeNames      explode('|'$content);
  63.                 $suggestedNames = array();
  64.                 foreach ($typeNames as $i => $typeName{
  65.                     $suggestedName = Common::suggestType($typeName);
  66.                     if (in_array($suggestedName$suggestedNames=== false{
  67.                         $suggestedNames[$suggestedName;
  68.                     }
  69.                 }
  70.  
  71.                 $suggestedType implode('|'$suggestedNames);
  72.                 if ($content !== $suggestedType{
  73.                     $error 'Expected "%s" but found "%s" for function return type';
  74.                     $data  = array(
  75.                               $suggestedType,
  76.                               $content,
  77.                              );
  78.                     $fix   $phpcsFile->addFixableError($error$return'InvalidReturn'$data);
  79.                     if ($fix === true{
  80.                         $phpcsFile->fixer->replaceToken(($return + 2)$suggestedType);
  81.                     }
  82.                 }
  83.  
  84.                 // Support both a return type and a description. The return type
  85.                 // is anything up to the first space.
  86.                 $returnParts explode(' '$content2);
  87.                 $returnType  $returnParts[0];
  88.  
  89.                 // If the return type is void, make sure there is
  90.                 // no return statement in the function.
  91.                 if ($returnType === 'void'{
  92.                     if (isset($tokens[$stackPtr]['scope_closer']=== true{
  93.                         $endToken $tokens[$stackPtr]['scope_closer'];
  94.                         for ($returnToken $stackPtr$returnToken $endToken$returnToken++{
  95.                             if ($tokens[$returnToken]['code'=== T_CLOSURE{
  96.                                 $returnToken $tokens[$returnToken]['scope_closer'];
  97.                                 continue;
  98.                             }
  99.  
  100.                             if ($tokens[$returnToken]['code'=== T_RETURN
  101.                                 || $tokens[$returnToken]['code'=== T_YIELD
  102.                             {
  103.                                 break;
  104.                             }
  105.                         }
  106.  
  107.                         if ($returnToken !== $endToken{
  108.                             // If the function is not returning anything, just
  109.                             // exiting, then there is no problem.
  110.                             $semicolon $phpcsFile->findNext(T_WHITESPACE($returnToken + 1)nulltrue);
  111.                             if ($tokens[$semicolon]['code'!== T_SEMICOLON{
  112.                                 $error 'Function return type is void, but function contains return statement';
  113.                                 $phpcsFile->addError($error$return'InvalidReturnVoid');
  114.                             }
  115.                         }
  116.                     }//end if
  117.                 else if ($returnType !== 'mixed'{
  118.                     // If return type is not void, there needs to be a return statement
  119.                     // somewhere in the function that returns something.
  120.                     if (isset($tokens[$stackPtr]['scope_closer']=== true{
  121.                         $endToken    $tokens[$stackPtr]['scope_closer'];
  122.                         $returnToken $phpcsFile->findNext(array(T_RETURNT_YIELD)$stackPtr$endToken);
  123.                         if ($returnToken === false{
  124.                             $error 'Function return type is not void, but function has no return statement';
  125.                             $phpcsFile->addError($error$return'InvalidNoReturn');
  126.                         else {
  127.                             $semicolon $phpcsFile->findNext(T_WHITESPACE($returnToken + 1)nulltrue);
  128.                             if ($tokens[$semicolon]['code'=== T_SEMICOLON{
  129.                                 $error 'Function return type is not void, but function is returning void here';
  130.                                 $phpcsFile->addError($error$returnToken'InvalidReturnNotVoid');
  131.                             }
  132.                         }
  133.                     }
  134.                 }//end if
  135.             }//end if
  136.         else {
  137.             $error 'Missing @return tag in function comment';
  138.             $phpcsFile->addError($error$tokens[$commentStart]['comment_closer']'MissingReturn');
  139.         }//end if
  140.  
  141.     }//end processReturn()
  142.  
  143.  
  144.     /**
  145.      * Process any throw tags that this function comment has.
  146.      *
  147.      * @param PHP_CodeSniffer_File $phpcsFile    The file being scanned.
  148.      * @param int                  $stackPtr     The position of the current token
  149.      *                                            in the stack passed in $tokens.
  150.      * @param int                  $commentStart The position in the stack where the comment started.
  151.      *
  152.      * @return void 
  153.      */
  154.     protected function processThrows(File $phpcsFile$stackPtr$commentStart)
  155.     {
  156.         $tokens $phpcsFile->getTokens();
  157.  
  158.         $throws = array();
  159.         foreach ($tokens[$commentStart]['comment_tags'as $pos => $tag{
  160.             if ($tokens[$tag]['content'!== '@throws'{
  161.                 continue;
  162.             }
  163.  
  164.             $exception = null;
  165.             $comment   = null;
  166.             if ($tokens[($tag + 2)]['code'=== T_DOC_COMMENT_STRING{
  167.                 $matches = array();
  168.                 preg_match('/([^\s]+)(?:\s+(.*))?/'$tokens[($tag + 2)]['content']$matches);
  169.                 $exception $matches[1];
  170.                 if (isset($matches[2]=== true && trim($matches[2]!== ''{
  171.                     $comment $matches[2];
  172.                 }
  173.             }
  174.  
  175.             if ($exception === null{
  176.                 $error 'Exception type and comment missing for @throws tag in function comment';
  177.                 $phpcsFile->addError($error$tag'InvalidThrows');
  178.             else if ($comment === null{
  179.                 $error 'Comment missing for @throws tag in function comment';
  180.                 $phpcsFile->addError($error$tag'EmptyThrows');
  181.             else {
  182.                 // Any strings until the next tag belong to this comment.
  183.                 if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]=== true{
  184.                     $end $tokens[$commentStart]['comment_tags'][($pos + 1)];
  185.                 else {
  186.                     $end $tokens[$commentStart]['comment_closer'];
  187.                 }
  188.  
  189.                 for ($i ($tag + 3)$i $end$i++{
  190.                     if ($tokens[$i]['code'=== T_DOC_COMMENT_STRING{
  191.                         $comment .= ' '.$tokens[$i]['content'];
  192.                     }
  193.                 }
  194.  
  195.                 // Starts with a capital letter and ends with a fullstop.
  196.                 $firstChar $comment{0};
  197.                 if (strtoupper($firstChar!== $firstChar{
  198.                     $error '@throws tag comment must start with a capital letter';
  199.                     $phpcsFile->addError($error($tag + 2)'ThrowsNotCapital');
  200.                 }
  201.  
  202.                 $lastChar substr($comment-1);
  203.                 if ($lastChar !== '.'{
  204.                     $error '@throws tag comment must end with a full stop';
  205.                     $phpcsFile->addError($error($tag + 2)'ThrowsNoFullStop');
  206.                 }
  207.             }//end if
  208.         }//end foreach
  209.  
  210.     }//end processThrows()
  211.  
  212.  
  213.     /**
  214.      * Process the function parameter comments.
  215.      *
  216.      * @param PHP_CodeSniffer_File $phpcsFile    The file being scanned.
  217.      * @param int                  $stackPtr     The position of the current token
  218.      *                                            in the stack passed in $tokens.
  219.      * @param int                  $commentStart The position in the stack where the comment started.
  220.      *
  221.      * @return void 
  222.      */
  223.     protected function processParams(File $phpcsFile$stackPtr$commentStart)
  224.     {
  225.         $tokens $phpcsFile->getTokens();
  226.  
  227.         $params  = array();
  228.         $maxType = 0;
  229.         $maxVar  = 0;
  230.         foreach ($tokens[$commentStart]['comment_tags'as $pos => $tag{
  231.             if ($tokens[$tag]['content'!== '@param'{
  232.                 continue;
  233.             }
  234.  
  235.             $type         '';
  236.             $typeSpace    = 0;
  237.             $var          '';
  238.             $varSpace     = 0;
  239.             $comment      '';
  240.             $commentLines = array();
  241.             if ($tokens[($tag + 2)]['code'=== T_DOC_COMMENT_STRING{
  242.                 $matches = array();
  243.                 preg_match('/([^$&.]+)(?:((?:\.\.\.)?(?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/'$tokens[($tag + 2)]['content']$matches);
  244.  
  245.                 $typeLen   strlen($matches[1]);
  246.                 $type      trim($matches[1]);
  247.                 $typeSpace ($typeLen strlen($type));
  248.                 $typeLen   strlen($type);
  249.                 if ($typeLen $maxType{
  250.                     $maxType $typeLen;
  251.                 }
  252.  
  253.                 if (isset($matches[2]=== true{
  254.                     $var    $matches[2];
  255.                     $varLen strlen($var);
  256.                     if ($varLen $maxVar{
  257.                         $maxVar $varLen;
  258.                     }
  259.  
  260.                     if (isset($matches[4]=== true{
  261.                         $varSpace       strlen($matches[3]);
  262.                         $comment        $matches[4];
  263.                         $commentLines[= array(
  264.                                            'comment' => $comment,
  265.                                            'token'   => ($tag + 2),
  266.                                            'indent'  => $varSpace,
  267.                                           );
  268.  
  269.                         // Any strings until the next tag belong to this comment.
  270.                         if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]=== true{
  271.                             $end $tokens[$commentStart]['comment_tags'][($pos + 1)];
  272.                         else {
  273.                             $end $tokens[$commentStart]['comment_closer'];
  274.                         }
  275.  
  276.                         for ($i ($tag + 3)$i $end$i++{
  277.                             if ($tokens[$i]['code'=== T_DOC_COMMENT_STRING{
  278.                                 $indent = 0;
  279.                                 if ($tokens[($i - 1)]['code'=== T_DOC_COMMENT_WHITESPACE{
  280.                                     $indent strlen($tokens[($i - 1)]['content']);
  281.                                 }
  282.  
  283.                                 $comment       .= ' '.$tokens[$i]['content'];
  284.                                 $commentLines[= array(
  285.                                                    'comment' => $tokens[$i]['content'],
  286.                                                    'token'   => $i,
  287.                                                    'indent'  => $indent,
  288.                                                   );
  289.                             }
  290.                         }
  291.                     else {
  292.                         $error 'Missing parameter comment';
  293.                         $phpcsFile->addError($error$tag'MissingParamComment');
  294.                         $commentLines[= array('comment' => '');
  295.                     }//end if
  296.                 else {
  297.                     $error 'Missing parameter name';
  298.                     $phpcsFile->addError($error$tag'MissingParamName');
  299.                 }//end if
  300.             else {
  301.                 $error 'Missing parameter type';
  302.                 $phpcsFile->addError($error$tag'MissingParamType');
  303.             }//end if
  304.  
  305.             $params[= array(
  306.                          'tag'          => $tag,
  307.                          'type'         => $type,
  308.                          'var'          => $var,
  309.                          'comment'      => $comment,
  310.                          'commentLines' => $commentLines,
  311.                          'type_space'   => $typeSpace,
  312.                          'var_space'    => $varSpace,
  313.                         );
  314.         }//end foreach
  315.  
  316.         $realParams  $phpcsFile->getMethodParameters($stackPtr);
  317.         $foundParams = array();
  318.  
  319.         // We want to use ... for all variable length arguments, so added
  320.         // this prefix to the variable name so comparisons are easier.
  321.         foreach ($realParams as $pos => $param{
  322.             if ($param['variable_length'=== true{
  323.                 $realParams[$pos]['name''...'.$realParams[$pos]['name'];
  324.             }
  325.         }
  326.  
  327.         foreach ($params as $pos => $param{
  328.             // If the type is empty, the whole line is empty.
  329.             if ($param['type'=== ''{
  330.                 continue;
  331.             }
  332.  
  333.             // Check the param type value.
  334.             $typeNames explode('|'$param['type']);
  335.             foreach ($typeNames as $typeName{
  336.                 $suggestedName = Common::suggestType($typeName);
  337.                 if ($typeName !== $suggestedName{
  338.                     $error 'Expected "%s" but found "%s" for parameter type';
  339.                     $data  = array(
  340.                               $suggestedName,
  341.                               $typeName,
  342.                              );
  343.  
  344.                     $fix $phpcsFile->addFixableError($error$param['tag']'IncorrectParamVarName'$data);
  345.                     if ($fix === true{
  346.                         $content  $suggestedName;
  347.                         $content .= str_repeat(' '$param['type_space']);
  348.                         $content .= $param['var'];
  349.                         $content .= str_repeat(' '$param['var_space']);
  350.                         if (isset($param['commentLines'][0]=== true{
  351.                             $content .= $param['commentLines'][0]['comment'];
  352.                         }
  353.  
  354.                         $phpcsFile->fixer->replaceToken(($param['tag'+ 2)$content);
  355.                     }
  356.                 else if (count($typeNames=== 1{
  357.                     // Check type hint for array and custom type.
  358.                     $suggestedTypeHint '';
  359.                     if (strpos($suggestedName'array'!== false || substr($suggestedName-2=== '[]'{
  360.                         $suggestedTypeHint 'array';
  361.                     else if (strpos($suggestedName'callable'!== false{
  362.                         $suggestedTypeHint 'callable';
  363.                     else if (strpos($suggestedName'callback'!== false{
  364.                         $suggestedTypeHint 'callable';
  365.                     else if (in_array($typeNameCommon::$allowedTypes=== false{
  366.                         $suggestedTypeHint $suggestedName;
  367.                     else if (version_compare(PHP_VERSION'7.0.0'>= 0{
  368.                         if ($typeName === 'string'{
  369.                             $suggestedTypeHint 'string';
  370.                         else if ($typeName === 'int' || $typeName === 'integer'{
  371.                             $suggestedTypeHint 'int';
  372.                         else if ($typeName === 'float'{
  373.                             $suggestedTypeHint 'float';
  374.                         else if ($typeName === 'bool' || $typeName === 'boolean'{
  375.                             $suggestedTypeHint 'bool';
  376.                         }
  377.                     }
  378.  
  379.                     if ($suggestedTypeHint !== '' && isset($realParams[$pos]=== true{
  380.                         $typeHint $realParams[$pos]['type_hint'];
  381.                         if ($typeHint === ''{
  382.                             $error 'Type hint "%s" missing for %s';
  383.                             $data  = array(
  384.                                       $suggestedTypeHint,
  385.                                       $param['var'],
  386.                                      );
  387.  
  388.                             $errorCode 'TypeHintMissing';
  389.                             if ($suggestedTypeHint === 'string'
  390.                                 || $suggestedTypeHint === 'int'
  391.                                 || $suggestedTypeHint === 'float'
  392.                                 || $suggestedTypeHint === 'bool'
  393.                             {
  394.                                 $errorCode 'Scalar'.$errorCode;
  395.                             }
  396.  
  397.                             $phpcsFile->addError($error$stackPtr$errorCode$data);
  398.                         else if ($typeHint !== substr($suggestedTypeHint(strlen($typeHint* -1))) {
  399.                             $error 'Expected type hint "%s"; found "%s" for %s';
  400.                             $data  = array(
  401.                                       $suggestedTypeHint,
  402.                                       $typeHint,
  403.                                       $param['var'],
  404.                                      );
  405.                             $phpcsFile->addError($error$stackPtr'IncorrectTypeHint'$data);
  406.                         }//end if
  407.                     else if ($suggestedTypeHint === '' && isset($realParams[$pos]=== true{
  408.                         $typeHint $realParams[$pos]['type_hint'];
  409.                         if ($typeHint !== ''{
  410.                             $error 'Unknown type hint "%s" found for %s';
  411.                             $data  = array(
  412.                                       $typeHint,
  413.                                       $param['var'],
  414.                                      );
  415.                             $phpcsFile->addError($error$stackPtr'InvalidTypeHint'$data);
  416.                         }
  417.                     }//end if
  418.                 }//end if
  419.             }//end foreach
  420.  
  421.             if ($param['var'=== ''{
  422.                 continue;
  423.             }
  424.  
  425.             $foundParams[$param['var'];
  426.  
  427.             // Check number of spaces after the type.
  428.             $spaces ($maxType strlen($param['type']+ 1);
  429.             if ($param['type_space'!== $spaces{
  430.                 $error 'Expected %s spaces after parameter type; %s found';
  431.                 $data  = array(
  432.                           $spaces,
  433.                           $param['type_space'],
  434.                          );
  435.  
  436.                 $fix $phpcsFile->addFixableError($error$param['tag']'SpacingAfterParamType'$data);
  437.                 if ($fix === true{
  438.                     $phpcsFile->fixer->beginChangeset();
  439.  
  440.                     $content  $param['type'];
  441.                     $content .= str_repeat(' '$spaces);
  442.                     $content .= $param['var'];
  443.                     $content .= str_repeat(' '$param['var_space']);
  444.                     $content .= $param['commentLines'][0]['comment'];
  445.                     $phpcsFile->fixer->replaceToken(($param['tag'+ 2)$content);
  446.  
  447.                     // Fix up the indent of additional comment lines.
  448.                     foreach ($param['commentLines'as $lineNum => $line{
  449.                         if ($lineNum === 0
  450.                             || $param['commentLines'][$lineNum]['indent'=== 0
  451.                         {
  452.                             continue;
  453.                         }
  454.  
  455.                         $newIndent ($param['commentLines'][$lineNum]['indent'$spaces $param['type_space']);
  456.                         $phpcsFile->fixer->replaceToken(
  457.                             ($param['commentLines'][$lineNum]['token'- 1),
  458.                             str_repeat(' '$newIndent)
  459.                         );
  460.                     }
  461.  
  462.                     $phpcsFile->fixer->endChangeset();
  463.                 }//end if
  464.             }//end if
  465.  
  466.             // Make sure the param name is correct.
  467.             if (isset($realParams[$pos]=== true{
  468.                 $realName $realParams[$pos]['name'];
  469.                 if ($realName !== $param['var']{
  470.                     $code 'ParamNameNoMatch';
  471.                     $data = array(
  472.                              $param['var'],
  473.                              $realName,
  474.                             );
  475.  
  476.                     $error 'Doc comment for parameter %s does not match ';
  477.                     if (strtolower($param['var']=== strtolower($realName)) {
  478.                         $error .= 'case of ';
  479.                         $code   'ParamNameNoCaseMatch';
  480.                     }
  481.  
  482.                     $error .= 'actual variable name %s';
  483.  
  484.                     $phpcsFile->addError($error$param['tag']$code$data);
  485.                 }
  486.             else if (substr($param['var']-4!== ',...'{
  487.                 // We must have an extra parameter comment.
  488.                 $error 'Superfluous parameter comment';
  489.                 $phpcsFile->addError($error$param['tag']'ExtraParamComment');
  490.             }//end if
  491.  
  492.             if ($param['comment'=== ''{
  493.                 continue;
  494.             }
  495.  
  496.             // Check number of spaces after the var name.
  497.             $spaces ($maxVar strlen($param['var']+ 1);
  498.             if ($param['var_space'!== $spaces{
  499.                 $error 'Expected %s spaces after parameter name; %s found';
  500.                 $data  = array(
  501.                           $spaces,
  502.                           $param['var_space'],
  503.                          );
  504.  
  505.                 $fix $phpcsFile->addFixableError($error$param['tag']'SpacingAfterParamName'$data);
  506.                 if ($fix === true{
  507.                     $phpcsFile->fixer->beginChangeset();
  508.  
  509.                     $content  $param['type'];
  510.                     $content .= str_repeat(' '$param['type_space']);
  511.                     $content .= $param['var'];
  512.                     $content .= str_repeat(' '$spaces);
  513.                     $content .= $param['commentLines'][0]['comment'];
  514.                     $phpcsFile->fixer->replaceToken(($param['tag'+ 2)$content);
  515.  
  516.                     // Fix up the indent of additional comment lines.
  517.                     foreach ($param['commentLines'as $lineNum => $line{
  518.                         if ($lineNum === 0
  519.                             || $param['commentLines'][$lineNum]['indent'=== 0
  520.                         {
  521.                             continue;
  522.                         }
  523.  
  524.                         $newIndent ($param['commentLines'][$lineNum]['indent'$spaces $param['var_space']);
  525.                         $phpcsFile->fixer->replaceToken(
  526.                             ($param['commentLines'][$lineNum]['token'- 1),
  527.                             str_repeat(' '$newIndent)
  528.                         );
  529.                     }
  530.  
  531.                     $phpcsFile->fixer->endChangeset();
  532.                 }//end if
  533.             }//end if
  534.  
  535.             // Param comments must start with a capital letter and end with the full stop.
  536.             if (preg_match('/^(\p{Ll}|\P{L})/u'$param['comment']=== 1{
  537.                 $error 'Parameter comment must start with a capital letter';
  538.                 $phpcsFile->addError($error$param['tag']'ParamCommentNotCapital');
  539.             }
  540.  
  541.             $lastChar substr($param['comment']-1);
  542.             if ($lastChar !== '.'{
  543.                 $error 'Parameter comment must end with a full stop';
  544.                 $phpcsFile->addError($error$param['tag']'ParamCommentFullStop');
  545.             }
  546.         }//end foreach
  547.  
  548.         $realNames = array();
  549.         foreach ($realParams as $realParam{
  550.             $realNames[$realParam['name'];
  551.         }
  552.  
  553.         // Report missing comments.
  554.         $diff array_diff($realNames$foundParams);
  555.         foreach ($diff as $neededParam{
  556.             $error 'Doc comment for parameter "%s" missing';
  557.             $data  = array($neededParam);
  558.             $phpcsFile->addError($error$commentStart'MissingParamTag'$data);
  559.         }
  560.  
  561.     }//end processParams()
  562.  
  563.  
  564. }//end class

Documentation generated on Mon, 11 Mar 2019 14:53:27 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.