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

Source for file ClassDeclarationSniff.php

Documentation is available at ClassDeclarationSniff.php

  1. <?php
  2. /**
  3.  * Checks the declaration of the class and its inheritance is correct.
  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\PSR2\Sniffs\Classes;
  11.  
  12. use PHP_CodeSniffer\Standards\PEAR\Sniffs\Classes\ClassDeclarationSniff as PEARClassDeclarationSniff;
  13. use PHP_CodeSniffer\Util\Tokens;
  14. use PHP_CodeSniffer\Files\File;
  15.  
  16. class ClassDeclarationSniff extends PEARClassDeclarationSniff
  17. {
  18.  
  19.     /**
  20.      * The number of spaces code should be indented.
  21.      *
  22.      * @var integer 
  23.      */
  24.     public $indent = 4;
  25.  
  26.  
  27.     /**
  28.      * Processes this test, when one of its tokens is encountered.
  29.      *
  30.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  31.      * @param int                         $stackPtr  The position of the current token
  32.      *                                                in the stack passed in $tokens.
  33.      *
  34.      * @return void 
  35.      */
  36.     public function process(File $phpcsFile, $stackPtr)
  37.     {
  38.         // We want all the errors from the PEAR standard, plus some of our own.
  39.         parent::process($phpcsFile, $stackPtr);
  40.  
  41.         // Just in case.
  42.         $tokens = $phpcsFile->getTokens();
  43.         if (isset($tokens[$stackPtr]['scope_opener']) === false) {
  44.             return;
  45.         }
  46.  
  47.         $this->processOpen($phpcsFile, $stackPtr);
  48.         $this->processClose($phpcsFile, $stackPtr);
  49.  
  50.     }//end process()
  51.  
  52.  
  53.     /**
  54.      * Processes the opening section of a class declaration.
  55.      *
  56.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  57.      * @param int                         $stackPtr  The position of the current token
  58.      *                                                in the stack passed in $tokens.
  59.      *
  60.      * @return void 
  61.      */
  62.     public function processOpen(File $phpcsFile, $stackPtr)
  63.     {
  64.         $tokens       = $phpcsFile->getTokens();
  65.         $stackPtrType = strtolower($tokens[$stackPtr]['content']);
  66.  
  67.         // Check alignment of the keyword and braces.
  68.         if ($tokens[($stackPtr - 1)]['code'] === T_WHITESPACE) {
  69.             $prevContent = $tokens[($stackPtr - 1)]['content'];
  70.             if ($prevContent !== $phpcsFile->eolChar) {
  71.                 $blankSpace = substr($prevContent, strpos($prevContent, $phpcsFile->eolChar));
  72.                 $spaces     = strlen($blankSpace);
  73.  
  74.                 if (in_array($tokens[($stackPtr - 2)]['code'], array(T_ABSTRACT, T_FINAL)) === true
  75.                     && $spaces !== 1
  76.                 ) {
  77.                     $prevContent = strtolower($tokens[($stackPtr - 2)]['content']);
  78.                     $error       = 'Expected 1 space between %s and %s keywords; %s found';
  79.                     $data        = array(
  80.                                     $prevContent,
  81.                                     $stackPtrType,
  82.                                     $spaces,
  83.                                    );
  84.  
  85.                     $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeKeyword', $data);
  86.                     if ($fix === true) {
  87.                         $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' ');
  88.                     }
  89.                 }
  90.             } else if ($tokens[($stackPtr - 2)]['code'] === T_ABSTRACT
  91.                 || $tokens[($stackPtr - 2)]['code'] === T_FINAL
  92.             ) {
  93.                 $prevContent = strtolower($tokens[($stackPtr - 2)]['content']);
  94.                 $error       = 'Expected 1 space between %s and %s keywords; newline found';
  95.                 $data        = array(
  96.                                 $prevContent,
  97.                                 $stackPtrType,
  98.                                );
  99.  
  100.                 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NewlineBeforeKeyword', $data);
  101.                 if ($fix === true) {
  102.                     $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' ');
  103.                 }
  104.             }//end if
  105.         }//end if
  106.  
  107.         // We'll need the indent of the class/interface declaration for later.
  108.         $classIndent = 0;
  109.         for ($i = ($stackPtr - 1); $i > 0; $i--) {
  110.             if ($tokens[$i]['line'] === $tokens[$stackPtr]['line']) {
  111.                 continue;
  112.             }
  113.  
  114.             // We changed lines.
  115.             if ($tokens[($i + 1)]['code'] === T_WHITESPACE) {
  116.                 $classIndent = strlen($tokens[($i + 1)]['content']);
  117.             }
  118.  
  119.             break;
  120.         }
  121.  
  122.         $className = $phpcsFile->findNext(T_STRING, $stackPtr);
  123.  
  124.         // Spacing of the keyword.
  125.         $gap = $tokens[($stackPtr + 1)]['content'];
  126.         if (strlen($gap) !== 1) {
  127.             $found = strlen($gap);
  128.             $error = 'Expected 1 space between %s keyword and %s name; %s found';
  129.             $data  = array(
  130.                       $stackPtrType,
  131.                       $stackPtrType,
  132.                       $found,
  133.                      );
  134.  
  135.             $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterKeyword', $data);
  136.             if ($fix === true) {
  137.                 $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' ');
  138.             }
  139.         }
  140.  
  141.         // Check after the class/interface name.
  142.         if ($tokens[($className + 2)]['line'] === $tokens[$className]['line']) {
  143.             $gap = $tokens[($className + 1)]['content'];
  144.             if (strlen($gap) !== 1) {
  145.                 $found = strlen($gap);
  146.                 $error = 'Expected 1 space after %s name; %s found';
  147.                 $data  = array(
  148.                           $stackPtrType,
  149.                           $found,
  150.                          );
  151.  
  152.                 $fix = $phpcsFile->addFixableError($error, $className, 'SpaceAfterName', $data);
  153.                 if ($fix === true) {
  154.                     $phpcsFile->fixer->replaceToken(($className + 1), ' ');
  155.                 }
  156.             }
  157.         }
  158.  
  159.         $openingBrace = $tokens[$stackPtr]['scope_opener'];
  160.  
  161.         // Check positions of the extends and implements keywords.
  162.         foreach (array('extends', 'implements') as $keywordType) {
  163.             $keyword = $phpcsFile->findNext(constant('T_'.strtoupper($keywordType)), ($stackPtr + 1), $openingBrace);
  164.             if ($keyword !== false) {
  165.                 if ($tokens[$keyword]['line'] !== $tokens[$stackPtr]['line']) {
  166.                     $error = 'The '.$keywordType.' keyword must be on the same line as the %s name';
  167.                     $data  = array($stackPtrType);
  168.                     $fix   = $phpcsFile->addFixableError($error, $keyword, ucfirst($keywordType).'Line', $data);
  169.                     if ($fix === true) {
  170.                         $phpcsFile->fixer->beginChangeset();
  171.                         for ($i = ($stackPtr + 1); $i < $keyword; $i++) {
  172.                             if ($tokens[$i]['line'] !== $tokens[($i + 1)]['line']) {
  173.                                 $phpcsFile->fixer->substrToken($i, 0, (strlen($phpcsFile->eolChar) * -1));
  174.                             }
  175.                         }
  176.  
  177.                         $phpcsFile->fixer->addContentBefore($keyword, ' ');
  178.                         $phpcsFile->fixer->endChangeset();
  179.                     }
  180.                 } else {
  181.                     // Check the whitespace before. Whitespace after is checked
  182.                     // later by looking at the whitespace before the first class name
  183.                     // in the list.
  184.                     $gap = strlen($tokens[($keyword - 1)]['content']);
  185.                     if ($gap !== 1) {
  186.                         $error = 'Expected 1 space before '.$keywordType.' keyword; %s found';
  187.                         $data  = array($gap);
  188.                         $fix   = $phpcsFile->addFixableError($error, $keyword, 'SpaceBefore'.ucfirst($keywordType), $data);
  189.                         if ($fix === true) {
  190.                             $phpcsFile->fixer->replaceToken(($keyword - 1), ' ');
  191.                         }
  192.                     }
  193.                 }//end if
  194.             }//end if
  195.         }//end foreach
  196.  
  197.         // Check each of the extends/implements class names. If the extends/implements
  198.         // keyword is the last content on the line, it means we need to check for
  199.         // the multi-line format, so we do not include the class names
  200.         // from the extends/implements list in the following check.
  201.         // Note that classes can only extend one other class, so they can't use a
  202.         // multi-line extends format, whereas an interface can extend multiple
  203.         // other interfaces, and so uses a multi-line extends format.
  204.         if ($tokens[$stackPtr]['code'] === T_INTERFACE) {
  205.             $keywordTokenType = T_EXTENDS;
  206.         } else {
  207.             $keywordTokenType = T_IMPLEMENTS;
  208.         }
  209.  
  210.         $implements          = $phpcsFile->findNext($keywordTokenType, ($stackPtr + 1), $openingBrace);
  211.         $multiLineImplements = false;
  212.         if ($implements !== false) {
  213.             $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($openingBrace - 1), $implements, true);
  214.             if ($tokens[$prev]['line'] !== $tokens[$implements]['line']) {
  215.                 $multiLineImplements = true;
  216.             }
  217.         }
  218.  
  219.         $find = array(
  220.                  T_STRING,
  221.                  $keywordTokenType,
  222.                 );
  223.  
  224.         $classNames = array();
  225.         $nextClass  = $phpcsFile->findNext($find, ($className + 2), ($openingBrace - 1));
  226.         while ($nextClass !== false) {
  227.             $classNames[] = $nextClass;
  228.             $nextClass    = $phpcsFile->findNext($find, ($nextClass + 1), ($openingBrace - 1));
  229.         }
  230.  
  231.         $classCount         = count($classNames);
  232.         $checkingImplements = false;
  233.         $implementsToken    = null;
  234.         foreach ($classNames as $i => $className) {
  235.             if ($tokens[$className]['code'] === $keywordTokenType) {
  236.                 $checkingImplements = true;
  237.                 $implementsToken    = $className;
  238.                 continue;
  239.             }
  240.  
  241.             if ($checkingImplements === true
  242.                 && $multiLineImplements === true
  243.                 && ($tokens[($className - 1)]['code'] !== T_NS_SEPARATOR
  244.                 || $tokens[($className - 2)]['code'] !== T_STRING)
  245.             ) {
  246.                 $prev = $phpcsFile->findPrevious(
  247.                     array(
  248.                      T_NS_SEPARATOR,
  249.                      T_WHITESPACE,
  250.                     ),
  251.                     ($className - 1),
  252.                     $implements,
  253.                     true
  254.                 );
  255.  
  256.                 if ($prev === $implementsToken && $tokens[$className]['line'] !== ($tokens[$prev]['line'] + 1)) {
  257.                     if ($keywordTokenType === T_EXTENDS) {
  258.                         $error = 'The first item in a multi-line extends list must be on the line following the extends keyword';
  259.                         $fix   = $phpcsFile->addFixableError($error, $className, 'FirstExtendsInterfaceSameLine');
  260.                     } else {
  261.                         $error = 'The first item in a multi-line implements list must be on the line following the implements keyword';
  262.                         $fix   = $phpcsFile->addFixableError($error, $className, 'FirstInterfaceSameLine');
  263.                     }
  264.  
  265.                     if ($fix === true) {
  266.                         $phpcsFile->fixer->beginChangeset();
  267.                         for ($i = ($prev + 1); $i < $className; $i++) {
  268.                             if ($tokens[$i]['code'] !== T_WHITESPACE) {
  269.                                 break;
  270.                             }
  271.  
  272.                             $phpcsFile->fixer->replaceToken($i, '');
  273.                         }
  274.  
  275.                         $phpcsFile->fixer->addNewline($prev);
  276.                         $phpcsFile->fixer->endChangeset();
  277.                     }
  278.                 } else if ($tokens[$prev]['line'] !== ($tokens[$className]['line'] - 1)) {
  279.                     if ($keywordTokenType === T_EXTENDS) {
  280.                         $error = 'Only one interface may be specified per line in a multi-line extends declaration';
  281.                         $fix   = $phpcsFile->addFixableError($error, $className, 'ExtendsInterfaceSameLine');
  282.                     } else {
  283.                         $error = 'Only one interface may be specified per line in a multi-line implements declaration';
  284.                         $fix   = $phpcsFile->addFixableError($error, $className, 'InterfaceSameLine');
  285.                     }
  286.  
  287.                     if ($fix === true) {
  288.                         $phpcsFile->fixer->beginChangeset();
  289.                         for ($i = ($prev + 1); $i < $className; $i++) {
  290.                             if ($tokens[$i]['code'] !== T_WHITESPACE) {
  291.                                 break;
  292.                             }
  293.  
  294.                             $phpcsFile->fixer->replaceToken($i, '');
  295.                         }
  296.  
  297.                         $phpcsFile->fixer->addNewline($prev);
  298.                         $phpcsFile->fixer->endChangeset();
  299.                     }
  300.                 } else {
  301.                     $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($className - 1), $implements);
  302.                     if ($tokens[$prev]['line'] !== $tokens[$className]['line']) {
  303.                         $found = 0;
  304.                     } else {
  305.                         $found = strlen($tokens[$prev]['content']);
  306.                     }
  307.  
  308.                     $expected = ($classIndent + $this->indent);
  309.                     if ($found !== $expected) {
  310.                         $error = 'Expected %s spaces before interface name; %s found';
  311.                         $data  = array(
  312.                                   $expected,
  313.                                   $found,
  314.                                  );
  315.                         $fix   = $phpcsFile->addFixableError($error, $className, 'InterfaceWrongIndent', $data);
  316.                         if ($fix === true) {
  317.                             $padding = str_repeat(' ', $expected);
  318.                             if ($found === 0) {
  319.                                 $phpcsFile->fixer->addContent($prev, $padding);
  320.                             } else {
  321.                                 $phpcsFile->fixer->replaceToken($prev, $padding);
  322.                             }
  323.                         }
  324.                     }
  325.                 }//end if
  326.             } else if ($tokens[($className - 1)]['code'] !== T_NS_SEPARATOR
  327.                 || $tokens[($className - 2)]['code'] !== T_STRING
  328.             ) {
  329.                 // Not part of a longer fully qualified class name.
  330.                 if ($tokens[($className - 1)]['code'] === T_COMMA
  331.                     || ($tokens[($className - 1)]['code'] === T_NS_SEPARATOR
  332.                     && $tokens[($className - 2)]['code'] === T_COMMA)
  333.                 ) {
  334.                     $error = 'Expected 1 space before "%s"; 0 found';
  335.                     $data  = array($tokens[$className]['content']);
  336.                     $fix   = $phpcsFile->addFixableError($error, ($nextComma + 1), 'NoSpaceBeforeName', $data);
  337.                     if ($fix === true) {
  338.                         $phpcsFile->fixer->addContentBefore(($nextComma + 1), ' ');
  339.                     }
  340.                 } else {
  341.                     if ($tokens[($className - 1)]['code'] === T_NS_SEPARATOR) {
  342.                         $prev = ($className - 2);
  343.                     } else {
  344.                         $prev = ($className - 1);
  345.                     }
  346.  
  347.                     $spaceBefore = strlen($tokens[$prev]['content']);
  348.                     if ($spaceBefore !== 1) {
  349.                         $error = 'Expected 1 space before "%s"; %s found';
  350.                         $data  = array(
  351.                                   $tokens[$className]['content'],
  352.                                   $spaceBefore,
  353.                                  );
  354.  
  355.                         $fix = $phpcsFile->addFixableError($error, $className, 'SpaceBeforeName', $data);
  356.                         if ($fix === true) {
  357.                             $phpcsFile->fixer->replaceToken($prev, ' ');
  358.                         }
  359.                     }
  360.                 }//end if
  361.             }//end if
  362.  
  363.             if ($checkingImplements === true
  364.                 && $tokens[($className + 1)]['code'] !== T_NS_SEPARATOR
  365.                 && $tokens[($className + 1)]['code'] !== T_COMMA
  366.             ) {
  367.                 if ($i !== ($classCount - 1)) {
  368.                     // This is not the last class name, and the comma
  369.                     // is not where we expect it to be.
  370.                     if ($tokens[($className + 2)]['code'] !== $keywordTokenType) {
  371.                         $error = 'Expected 0 spaces between "%s" and comma; %s found';
  372.                         $data  = array(
  373.                                   $tokens[$className]['content'],
  374.                                   strlen($tokens[($className + 1)]['content']),
  375.                                  );
  376.  
  377.                         $fix = $phpcsFile->addFixableError($error, $className, 'SpaceBeforeComma', $data);
  378.                         if ($fix === true) {
  379.                             $phpcsFile->fixer->replaceToken(($className + 1), '');
  380.                         }
  381.                     }
  382.                 }
  383.  
  384.                 $nextComma = $phpcsFile->findNext(T_COMMA, $className);
  385.             } else {
  386.                 $nextComma = ($className + 1);
  387.             }//end if
  388.         }//end foreach
  389.  
  390.     }//end processOpen()
  391.  
  392.  
  393.     /**
  394.      * Processes the closing section of a class declaration.
  395.      *
  396.      * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  397.      * @param int                         $stackPtr  The position of the current token
  398.      *                                                in the stack passed in $tokens.
  399.      *
  400.      * @return void 
  401.      */
  402.     public function processClose(File $phpcsFile, $stackPtr)
  403.     {
  404.         $tokens = $phpcsFile->getTokens();
  405.  
  406.         // Check that the closing brace comes right after the code body.
  407.         $closeBrace  = $tokens[$stackPtr]['scope_closer'];
  408.         $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($closeBrace - 1), null, true);
  409.         if ($prevContent !== $tokens[$stackPtr]['scope_opener']
  410.             && $tokens[$prevContent]['line'] !== ($tokens[$closeBrace]['line'] - 1)
  411.         ) {
  412.             $error = 'The closing brace for the %s must go on the next line after the body';
  413.             $data  = array($tokens[$stackPtr]['content']);
  414.             $fix   = $phpcsFile->addFixableError($error, $closeBrace, 'CloseBraceAfterBody', $data);
  415.  
  416.             if ($fix === true) {
  417.                 $phpcsFile->fixer->beginChangeset();
  418.                 for ($i = ($prevContent + 1); $i < $closeBrace; $i++) {
  419.                     $phpcsFile->fixer->replaceToken($i, '');
  420.                 }
  421.  
  422.                 if (strpos($tokens[$prevContent]['content'], $phpcsFile->eolChar) === false) {
  423.                     $phpcsFile->fixer->replaceToken($closeBrace, $phpcsFile->eolChar.$tokens[$closeBrace]['content']);
  424.                 }
  425.  
  426.                 $phpcsFile->fixer->endChangeset();
  427.             }
  428.         }//end if
  429.  
  430.         // Check the closing brace is on it's own line, but allow
  431.         // for comments like "//end class".
  432.         $nextContent = $phpcsFile->findNext(array(T_WHITESPACE, T_COMMENT), ($closeBrace + 1), null, true);
  433.         if ($tokens[$nextContent]['content'] !== $phpcsFile->eolChar
  434.             && $tokens[$nextContent]['line'] === $tokens[$closeBrace]['line']
  435.         ) {
  436.             $type  = strtolower($tokens[$stackPtr]['content']);
  437.             $error = 'Closing %s brace must be on a line by itself';
  438.             $data  = array($type);
  439.             $phpcsFile->addError($error, $closeBrace, 'CloseBraceSameLine', $data);
  440.         }
  441.  
  442.     }//end processClose()
  443.  
  444.  
  445. }//end class

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