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

Source for file FileCommentSniff.php

Documentation is available at FileCommentSniff.php

  1. <?php
  2. /**
  3.  * Parses and verifies the doc comments for files.
  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\PEAR\Sniffs\Commenting;
  11.  
  12. use PHP_CodeSniffer\Sniffs\Sniff;
  13. use PHP_CodeSniffer\Files\File;
  14. use PHP_CodeSniffer\Util\Common;
  15.  
  16. class FileCommentSniff implements Sniff
  17. {
  18.  
  19.     /**
  20.      * Tags in correct order and related info.
  21.      *
  22.      * @var array 
  23.      */
  24.     protected $tags = array(
  25.                        '@category'   => array(
  26.                                          'required'       => true,
  27.                                          'allow_multiple' => false,
  28.                                         ),
  29.                        '@package'    => array(
  30.                                          'required'       => true,
  31.                                          'allow_multiple' => false,
  32.                                         ),
  33.                        '@subpackage' => array(
  34.                                          'required'       => false,
  35.                                          'allow_multiple' => false,
  36.                                         ),
  37.                        '@author'     => array(
  38.                                          'required'       => true,
  39.                                          'allow_multiple' => true,
  40.                                         ),
  41.                        '@copyright'  => array(
  42.                                          'required'       => false,
  43.                                          'allow_multiple' => true,
  44.                                         ),
  45.                        '@license'    => array(
  46.                                          'required'       => true,
  47.                                          'allow_multiple' => false,
  48.                                         ),
  49.                        '@version'    => array(
  50.                                          'required'       => false,
  51.                                          'allow_multiple' => false,
  52.                                         ),
  53.                        '@link'       => array(
  54.                                          'required'       => true,
  55.                                          'allow_multiple' => true,
  56.                                         ),
  57.                        '@see'        => array(
  58.                                          'required'       => false,
  59.                                          'allow_multiple' => true,
  60.                                         ),
  61.                        '@since'      => array(
  62.                                          'required'       => false,
  63.                                          'allow_multiple' => false,
  64.                                         ),
  65.                        '@deprecated' => array(
  66.                                          'required'       => false,
  67.                                          'allow_multiple' => false,
  68.                                         ),
  69.                       );
  70.  
  71.  
  72.     /**
  73.      * Returns an array of tokens this test wants to listen for.
  74.      *
  75.      * @return array 
  76.      */
  77.     public function register()
  78.     {
  79.         return array(T_OPEN_TAG);
  80.  
  81.     }//end register()
  82.  
  83.  
  84.     /**
  85.      * Processes this test, when one of its tokens is encountered.
  86.      *
  87.      * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  88.      * @param int                  $stackPtr  The position of the current token
  89.      *                                         in the stack passed in $tokens.
  90.      *
  91.      * @return int 
  92.      */
  93.     public function process(File $phpcsFile$stackPtr)
  94.     {
  95.         $tokens $phpcsFile->getTokens();
  96.  
  97.         // Find the next non whitespace token.
  98.         $commentStart $phpcsFile->findNext(T_WHITESPACE($stackPtr + 1)nulltrue);
  99.  
  100.         // Allow declare() statements at the top of the file.
  101.         if ($tokens[$commentStart]['code'=== T_DECLARE{
  102.             $semicolon    $phpcsFile->findNext(T_SEMICOLON($commentStart + 1));
  103.             $commentStart $phpcsFile->findNext(T_WHITESPACE($semicolon + 1)nulltrue);
  104.         }
  105.  
  106.         // Ignore vim header.
  107.         if ($tokens[$commentStart]['code'=== T_COMMENT{
  108.             if (strstr($tokens[$commentStart]['content']'vim:'!== false{
  109.                 $commentStart $phpcsFile->findNext(
  110.                     T_WHITESPACE,
  111.                     ($commentStart + 1),
  112.                     null,
  113.                     true
  114.                 );
  115.             }
  116.         }
  117.  
  118.         $errorToken ($stackPtr + 1);
  119.         if (isset($tokens[$errorToken]=== false{
  120.             $errorToken--;
  121.         }
  122.  
  123.         if ($tokens[$commentStart]['code'=== T_CLOSE_TAG{
  124.             // We are only interested if this is the first open tag.
  125.             return ($phpcsFile->numTokens + 1);
  126.         else if ($tokens[$commentStart]['code'=== T_COMMENT{
  127.             $error 'You must use "/**" style comments for a file comment';
  128.             $phpcsFile->addError($error$errorToken'WrongStyle');
  129.             $phpcsFile->recordMetric($stackPtr'File has doc comment''yes');
  130.             return ($phpcsFile->numTokens + 1);
  131.         else if ($commentStart === false
  132.             || $tokens[$commentStart]['code'!== T_DOC_COMMENT_OPEN_TAG
  133.         {
  134.             $phpcsFile->addError('Missing file doc comment'$errorToken'Missing');
  135.             $phpcsFile->recordMetric($stackPtr'File has doc comment''no');
  136.             return ($phpcsFile->numTokens + 1);
  137.         }
  138.  
  139.         $commentEnd $tokens[$commentStart]['comment_closer'];
  140.  
  141.         $nextToken $phpcsFile->findNext(
  142.             T_WHITESPACE,
  143.             ($commentEnd + 1),
  144.             null,
  145.             true
  146.         );
  147.  
  148.         $ignore = array(
  149.                    T_CLASS,
  150.                    T_INTERFACE,
  151.                    T_TRAIT,
  152.                    T_FUNCTION,
  153.                    T_CLOSURE,
  154.                    T_PUBLIC,
  155.                    T_PRIVATE,
  156.                    T_PROTECTED,
  157.                    T_FINAL,
  158.                    T_STATIC,
  159.                    T_ABSTRACT,
  160.                    T_CONST,
  161.                    T_PROPERTY,
  162.                    T_INCLUDE,
  163.                    T_INCLUDE_ONCE,
  164.                    T_REQUIRE,
  165.                    T_REQUIRE_ONCE,
  166.                   );
  167.  
  168.         if (in_array($tokens[$nextToken]['code']$ignore=== true{
  169.             $phpcsFile->addError('Missing file doc comment'$stackPtr'Missing');
  170.             $phpcsFile->recordMetric($stackPtr'File has doc comment''no');
  171.             return ($phpcsFile->numTokens + 1);
  172.         }
  173.  
  174.         $phpcsFile->recordMetric($stackPtr'File has doc comment''yes');
  175.  
  176.         // Check the PHP Version, which should be in some text before the first tag.
  177.         $found = false;
  178.         for ($i ($commentStart + 1)$i $commentEnd$i++{
  179.             if ($tokens[$i]['code'=== T_DOC_COMMENT_TAG{
  180.                 break;
  181.             else if ($tokens[$i]['code'=== T_DOC_COMMENT_STRING
  182.                 && strstr(strtolower($tokens[$i]['content'])'php version'!== false
  183.             {
  184.                 $found = true;
  185.                 break;
  186.             }
  187.         }
  188.  
  189.         if ($found === false{
  190.             $error 'PHP version not specified';
  191.             $phpcsFile->addWarning($error$commentEnd'MissingVersion');
  192.         }
  193.  
  194.         // Check each tag.
  195.         $this->processTags($phpcsFile$stackPtr$commentStart);
  196.  
  197.         // Ignore the rest of the file.
  198.         return ($phpcsFile->numTokens + 1);
  199.  
  200.     }//end process()
  201.  
  202.  
  203.     /**
  204.      * Processes each required or optional tag.
  205.      *
  206.      * @param PHP_CodeSniffer_File $phpcsFile    The file being scanned.
  207.      * @param int                  $stackPtr     The position of the current token
  208.      *                                            in the stack passed in $tokens.
  209.      * @param int                  $commentStart Position in the stack where the comment started.
  210.      *
  211.      * @return void 
  212.      */
  213.     protected function processTags($phpcsFile$stackPtr$commentStart)
  214.     {
  215.         $tokens $phpcsFile->getTokens();
  216.  
  217.         if (get_class($this=== 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FileCommentSniff'{
  218.             $docBlock 'file';
  219.         else {
  220.             $docBlock 'class';
  221.         }
  222.  
  223.         $commentEnd $tokens[$commentStart]['comment_closer'];
  224.  
  225.         $foundTags = array();
  226.         $tagTokens = array();
  227.         foreach ($tokens[$commentStart]['comment_tags'as $tag{
  228.             $name $tokens[$tag]['content'];
  229.             if (isset($this->tags[$name]=== false{
  230.                 continue;
  231.             }
  232.  
  233.             if ($this->tags[$name]['allow_multiple'=== false && isset($tagTokens[$name]=== true{
  234.                 $error 'Only one %s tag is allowed in a %s comment';
  235.                 $data  = array(
  236.                           $name,
  237.                           $docBlock,
  238.                          );
  239.                 $phpcsFile->addError($error$tag'Duplicate'.ucfirst(substr($name1)).'Tag'$data);
  240.             }
  241.  
  242.             $foundTags[]        $name;
  243.             $tagTokens[$name][$tag;
  244.  
  245.             $string $phpcsFile->findNext(T_DOC_COMMENT_STRING$tag$commentEnd);
  246.             if ($string === false || $tokens[$string]['line'!== $tokens[$tag]['line']{
  247.                 $error 'Content missing for %s tag in %s comment';
  248.                 $data  = array(
  249.                           $name,
  250.                           $docBlock,
  251.                          );
  252.                 $phpcsFile->addError($error$tag'Empty'.ucfirst(substr($name1)).'Tag'$data);
  253.                 continue;
  254.             }
  255.         }//end foreach
  256.  
  257.         // Check if the tags are in the correct position.
  258.         $pos = 0;
  259.         foreach ($this->tags as $tag => $tagData{
  260.             if (isset($tagTokens[$tag]=== false{
  261.                 if ($tagData['required'=== true{
  262.                     $error 'Missing %s tag in %s comment';
  263.                     $data  = array(
  264.                               $tag,
  265.                               $docBlock,
  266.                              );
  267.                     $phpcsFile->addError($error$commentEnd'Missing'.ucfirst(substr($tag1)).'Tag'$data);
  268.                 }
  269.  
  270.                 continue;
  271.             else {
  272.                 $method 'process'.substr($tag1);
  273.                 if (method_exists($this$method=== true{
  274.                     // Process each tag if a method is defined.
  275.                     call_user_func(array($this$method)$phpcsFile$tagTokens[$tag]);
  276.                 }
  277.             }
  278.  
  279.             if (isset($foundTags[$pos]=== false{
  280.                 break;
  281.             }
  282.  
  283.             if ($foundTags[$pos!== $tag{
  284.                 $error 'The tag in position %s should be the %s tag';
  285.                 $data  = array(
  286.                           ($pos + 1),
  287.                           $tag,
  288.                          );
  289.                 $phpcsFile->addError($error$tokens[$commentStart]['comment_tags'][$pos]ucfirst(substr($tag1)).'TagOrder'$data);
  290.             }
  291.  
  292.             // Account for multiple tags.
  293.             $pos++;
  294.             while (isset($foundTags[$pos]=== true && $foundTags[$pos=== $tag{
  295.                 $pos++;
  296.             }
  297.         }//end foreach
  298.  
  299.     }//end processTags()
  300.  
  301.  
  302.     /**
  303.      * Process the category tag.
  304.      *
  305.      * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  306.      * @param array                $tags      The tokens for these tags.
  307.      *
  308.      * @return void 
  309.      */
  310.     protected function processCategory($phpcsFilearray $tags)
  311.     {
  312.         $tokens $phpcsFile->getTokens();
  313.         foreach ($tags as $tag{
  314.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  315.                 // No content.
  316.                 continue;
  317.             }
  318.  
  319.             $content $tokens[($tag + 2)]['content'];
  320.             if (Common::isUnderscoreName($content!== true{
  321.                 $newContent str_replace(' ''_'$content);
  322.                 $nameBits   explode('_'$newContent);
  323.                 $firstBit   array_shift($nameBits);
  324.                 $newName    ucfirst($firstBit).'_';
  325.                 foreach ($nameBits as $bit{
  326.                     if ($bit !== ''{
  327.                         $newName .= ucfirst($bit).'_';
  328.                     }
  329.                 }
  330.  
  331.                 $error     'Category name "%s" is not valid; consider "%s" instead';
  332.                 $validName trim($newName'_');
  333.                 $data      = array(
  334.                               $content,
  335.                               $validName,
  336.                              );
  337.                 $phpcsFile->addError($error$tag'InvalidCategory'$data);
  338.             }
  339.         }//end foreach
  340.  
  341.     }//end processCategory()
  342.  
  343.  
  344.     /**
  345.      * Process the package tag.
  346.      *
  347.      * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  348.      * @param array                $tags      The tokens for these tags.
  349.      *
  350.      * @return void 
  351.      */
  352.     protected function processPackage($phpcsFilearray $tags)
  353.     {
  354.         $tokens $phpcsFile->getTokens();
  355.         foreach ($tags as $tag{
  356.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  357.                 // No content.
  358.                 continue;
  359.             }
  360.  
  361.             $content $tokens[($tag + 2)]['content'];
  362.             if (Common::isUnderscoreName($content=== true{
  363.                 continue;
  364.             }
  365.  
  366.             $newContent str_replace(' ''_'$content);
  367.             $newContent trim($newContent'_');
  368.             $newContent preg_replace('/[^A-Za-z_]/'''$newContent);
  369.  
  370.             if ($newContent === ''{
  371.                 $error 'Package name "%s" is not valid';
  372.                 $data  = array($content);
  373.                 $phpcsFile->addError($error$tag'InvalidPackageValue'$data);
  374.             else {
  375.                 $nameBits explode('_'$newContent);
  376.                 $firstBit array_shift($nameBits);
  377.                 $newName  strtoupper($firstBit{0}).substr($firstBit1).'_';
  378.                 foreach ($nameBits as $bit{
  379.                     if ($bit !== ''{
  380.                         $newName .= strtoupper($bit{0}).substr($bit1).'_';
  381.                     }
  382.                 }
  383.  
  384.                 $error     'Package name "%s" is not valid; consider "%s" instead';
  385.                 $validName trim($newName'_');
  386.                 $data      = array(
  387.                               $content,
  388.                               $validName,
  389.                              );
  390.                 $phpcsFile->addError($error$tag'InvalidPackage'$data);
  391.             }//end if
  392.         }//end foreach
  393.  
  394.     }//end processPackage()
  395.  
  396.  
  397.     /**
  398.      * Process the subpackage tag.
  399.      *
  400.      * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  401.      * @param array                $tags      The tokens for these tags.
  402.      *
  403.      * @return void 
  404.      */
  405.     protected function processSubpackage($phpcsFilearray $tags)
  406.     {
  407.         $tokens $phpcsFile->getTokens();
  408.         foreach ($tags as $tag{
  409.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  410.                 // No content.
  411.                 continue;
  412.             }
  413.  
  414.             $content $tokens[($tag + 2)]['content'];
  415.             if (Common::isUnderscoreName($content=== true{
  416.                 continue;
  417.             }
  418.  
  419.             $newContent str_replace(' ''_'$content);
  420.             $nameBits   explode('_'$newContent);
  421.             $firstBit   array_shift($nameBits);
  422.             $newName    strtoupper($firstBit{0}).substr($firstBit1).'_';
  423.             foreach ($nameBits as $bit{
  424.                 if ($bit !== ''{
  425.                     $newName .= strtoupper($bit{0}).substr($bit1).'_';
  426.                 }
  427.             }
  428.  
  429.             $error     'Subpackage name "%s" is not valid; consider "%s" instead';
  430.             $validName trim($newName'_');
  431.             $data      = array(
  432.                           $content,
  433.                           $validName,
  434.                          );
  435.             $phpcsFile->addError($error$tag'InvalidSubpackage'$data);
  436.         }//end foreach
  437.  
  438.     }//end processSubpackage()
  439.  
  440.  
  441.     /**
  442.      * Process the author tag(s) that this header comment has.
  443.      *
  444.      * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  445.      * @param array                $tags      The tokens for these tags.
  446.      *
  447.      * @return void 
  448.      */
  449.     protected function processAuthor($phpcsFilearray $tags)
  450.     {
  451.         $tokens $phpcsFile->getTokens();
  452.         foreach ($tags as $tag{
  453.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  454.                 // No content.
  455.                 continue;
  456.             }
  457.  
  458.             $content $tokens[($tag + 2)]['content'];
  459.             $local   '\da-zA-Z-_+';
  460.             // Dot character cannot be the first or last character in the local-part.
  461.             $localMiddle $local.'.\w';
  462.             if (preg_match('/^([^<]*)\s+<(['.$local.'](['.$localMiddle.']*['.$local.'])*@[\da-zA-Z][-.\w]*[\da-zA-Z]\.[a-zA-Z]{2,7})>$/'$content=== 0{
  463.                 $error 'Content of the @author tag must be in the form "Display Name <username@example.com>"';
  464.                 $phpcsFile->addError($error$tag'InvalidAuthors');
  465.             }
  466.         }
  467.  
  468.     }//end processAuthor()
  469.  
  470.  
  471.     /**
  472.      * Process the copyright tags.
  473.      *
  474.      * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  475.      * @param array                $tags      The tokens for these tags.
  476.      *
  477.      * @return void 
  478.      */
  479.     protected function processCopyright($phpcsFilearray $tags)
  480.     {
  481.         $tokens $phpcsFile->getTokens();
  482.         foreach ($tags as $tag{
  483.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  484.                 // No content.
  485.                 continue;
  486.             }
  487.  
  488.             $content $tokens[($tag + 2)]['content'];
  489.             $matches = array();
  490.             if (preg_match('/^([0-9]{4})((.{1})([0-9]{4}))? (.+)$/'$content$matches!== 0{
  491.                 // Check earliest-latest year order.
  492.                 if ($matches[3!== ''{
  493.                     if ($matches[3!== '-'{
  494.                         $error 'A hyphen must be used between the earliest and latest year';
  495.                         $phpcsFile->addError($error$tag'CopyrightHyphen');
  496.                     }
  497.  
  498.                     if ($matches[4!== '' && $matches[4$matches[1]{
  499.                         $error = "Invalid year span \"$matches[1]$matches[3]$matches[4]\" found; consider \"$matches[4]-$matches[1]\" instead";
  500.                         $phpcsFile->addWarning($error$tag'InvalidCopyright');
  501.                     }
  502.                 }
  503.             else {
  504.                 $error '@copyright tag must contain a year and the name of the copyright holder';
  505.                 $phpcsFile->addError($error$tag'IncompleteCopyright');
  506.             }
  507.         }//end foreach
  508.  
  509.     }//end processCopyright()
  510.  
  511.  
  512.     /**
  513.      * Process the license tag.
  514.      *
  515.      * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  516.      * @param array                $tags      The tokens for these tags.
  517.      *
  518.      * @return void 
  519.      */
  520.     protected function processLicense($phpcsFilearray $tags)
  521.     {
  522.         $tokens $phpcsFile->getTokens();
  523.         foreach ($tags as $tag{
  524.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  525.                 // No content.
  526.                 continue;
  527.             }
  528.  
  529.             $content $tokens[($tag + 2)]['content'];
  530.             $matches = array();
  531.             preg_match('/^([^\s]+)\s+(.*)/'$content$matches);
  532.             if (count($matches!== 3{
  533.                 $error '@license tag must contain a URL and a license name';
  534.                 $phpcsFile->addError($error$tag'IncompleteLicense');
  535.             }
  536.         }
  537.  
  538.     }//end processLicense()
  539.  
  540.  
  541.     /**
  542.      * Process the version tag.
  543.      *
  544.      * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  545.      * @param array                $tags      The tokens for these tags.
  546.      *
  547.      * @return void 
  548.      */
  549.     protected function processVersion($phpcsFilearray $tags)
  550.     {
  551.         $tokens $phpcsFile->getTokens();
  552.         foreach ($tags as $tag{
  553.             if ($tokens[($tag + 2)]['code'!== T_DOC_COMMENT_STRING{
  554.                 // No content.
  555.                 continue;
  556.             }
  557.  
  558.             $content $tokens[($tag + 2)]['content'];
  559.             if (strstr($content'CVS:'=== false
  560.                 && strstr($content'SVN:'=== false
  561.                 && strstr($content'GIT:'=== false
  562.                 && strstr($content'HG:'=== false
  563.             {
  564.                 $error 'Invalid version "%s" in file comment; consider "CVS: <cvs_id>" or "SVN: <svn_id>" or "GIT: <git_id>" or "HG: <hg_id>" instead';
  565.                 $data  = array($content);
  566.                 $phpcsFile->addWarning($error$tag'InvalidVersion'$data);
  567.             }
  568.         }
  569.  
  570.     }//end processVersion()
  571.  
  572.  
  573. }//end class

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