diff --git a/Sniffs/Commenting/FunctionCommentThrowTagSniff.php b/Sniffs/Commenting/FunctionCommentThrowTagSniff.php new file mode 100644 index 00000000..c9d9f8d5 --- /dev/null +++ b/Sniffs/Commenting/FunctionCommentThrowTagSniff.php @@ -0,0 +1,271 @@ + + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version 1.0 + * @link http://pear.php.net/package/PHP_CodeSniffer_CakePHP + */ + +if (class_exists('PHP_CodeSniffer_Standards_AbstractScopeSniff', true) === false) { + $error = 'Class PHP_CodeSniffer_Standards_AbstractScopeSniff not found'; + throw new PHP_CodeSniffer_Exception($error); +} + +/** + * CakePHP_Sniffs_Commenting_FunctionCommentThrowTagSniff. + * + * Ensures the throws in the code are declared in the PHPDoc + * + * @category PHP + * @package PHP_CodeSniffer_CakePHP + * @author Juan Basso + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version 1.0 + * @link http://pear.php.net/package/PHP_CodeSniffer_CakePHP + */ +class CakePHP_Sniffs_Commenting_FunctionCommentThrowTagSniff extends PHP_CodeSniffer_Standards_AbstractScopeSniff { + +/** + * Constructs a CakePHP_Sniffs_Commenting_FunctionCommentThrowTagSniff. + */ + public function __construct() { + parent::__construct(array(T_FUNCTION), array(T_THROW)); + } + +/** + * Processes this test, when one of its tokens is encountered. + * + * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @return void + */ + protected function processTokenWithinScope(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $currScope) { + // Is this the first throw token within the current function scope? + // If so, we have to validate other throw tokens within the same scope. + $previousThrow = $phpcsFile->findPrevious(T_THROW, ($stackPtr - 1), $currScope); + if ($previousThrow !== false) { + return; + } + + // Parse the function comment. + $tokens = $phpcsFile->getTokens(); + $commentEnd = $phpcsFile->findPrevious(T_DOC_COMMENT, ($currScope - 1)); + $commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1); + $comment = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1)); + + try { + $this->commentParser = new PHP_CodeSniffer_CommentParser_FunctionCommentParser($comment, $phpcsFile); + $this->commentParser->parse(); + } catch (PHP_CodeSniffer_CommentParser_ParserException $e) { + $line = ($e->getLineWithinComment() + $commentStart); + $phpcsFile->addError($e->getMessage(), $line, 'FailedParse'); + return; + } + + // Find the position where the current function scope ends. + $currScopeEnd = 0; + if (isset($tokens[$currScope]['scope_closer']) === true) { + $currScopeEnd = $tokens[$currScope]['scope_closer']; + } + + // Find all the exception type token within the current scope. + $throwTokens = array(); + $currPos = $stackPtr; + if ($currScopeEnd !== 0) { + while ($currPos < $currScopeEnd && $currPos !== false) { + /* + If we can't find a NEW, we are probably throwing + a variable, so we ignore it, but they still need to + provide at least one @throws tag, even through we + don't know the exception class. + */ + + $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($currPos + 1), null, true); + if ($tokens[$nextToken]['code'] === T_NEW) { + $currException = $phpcsFile->findNext(array(T_STRING, T_NS_SEPARATOR), $currPos, $currScopeEnd, false, null, true); + if ($currException !== false) { + $exception = $tokens[$currException]['content']; + $i = $currException + 1; + while (in_array($tokens[$i]['code'], array(T_STRING, T_NS_SEPARATOR))) { + $exception .= $tokens[$i++]['content']; + } + $throwTokens[] = $exception; + } + } + + $currPos = $phpcsFile->findNext(T_THROW, ($currPos + 1), $currScopeEnd); + } + } + + $namespace = $this->_getNamespace($phpcsFile, $currScope); + $uses = $this->_readUses($phpcsFile); + $throwTokens = $this->_adjustThrows($throwTokens, $namespace, $uses); + + $throws = $this->commentParser->getThrows(); + if (empty($throws) === true) { + $error = 'Missing @throws tag in function comment'; + $phpcsFile->addError($error, $commentEnd, 'Missing'); + } else if (empty($throwTokens) === true) { + // If token count is zero, it means that only variables are being + // thrown, so we need at least one @throws tag (checked above). + // Nothing more to do. + return; + } else { + $throwTags = array(); + $lineNumber = array(); + foreach ($throws as $throw) { + $value = ltrim($throw->getValue(), '\\'); + $throwTags[] = $value; + $lineNumber[$value] = $throw->getLine(); + } + + $throwTags = array_unique($throwTags); + sort($throwTags); + + // Make sure @throws tag count matches throw token count. + $tokenCount = count($throwTokens); + $tagCount = count($throwTags); + if ($tokenCount !== $tagCount) { + $error = 'Expected %s @throws tag(s) in function comment; %s found'; + $data = array( + $tokenCount, + $tagCount, + ); + $phpcsFile->addError($error, $commentEnd, 'WrongNumber', $data); + return; + } else { + // Exception type in @throws tag must be thrown in the function. + foreach ($throwTags as $i => $throwTag) { + $errorPos = ($commentStart + $lineNumber[$throwTag]); + if (empty($throwTag) === false && $throwTag !== $throwTokens[$i]) { + $error = 'Expected "%s" but found "%s" for @throws tag exception'; + $data = array( + $throwTokens[$i], + $throwTag, + ); + $phpcsFile->addError($error, $errorPos, 'WrongType', $data); + } + } + } + } + } + +/** + * Find the class namespace. + * + * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. + * @param int $currScope Current scope + * @return string + */ + protected function _getNamespace(PHP_CodeSniffer_File $phpcsFile, $currScope) { + $nsPos = $phpcsFile->findPrevious(T_NAMESPACE, $currScope - 1); + if (!$nsPos) { + return ''; + } + + $tokens = $phpcsFile->getTokens(); + $i = $nsPos + 2; // Ignore whitespace + $ns = ''; + while (in_array($tokens[$i]['code'], array(T_STRING, T_NS_SEPARATOR))) { + $ns .= $tokens[$i]['content']; + $i++; + } + return $ns; + } + +/** + * Read the use declarations + * + * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. + * @return array + */ + protected function _readUses(PHP_CodeSniffer_File $phpcsFile) { + $pos = $phpcsFile->findNext(T_USE, 1); + if (!$pos) { + return array(); + } + + $tokens = $phpcsFile->getTokens(); + $pos += 2; // Ignore use keywork and whitespace + $uses = array(); + do { + $use = $alias = ''; + while (in_array($tokens[$pos]['code'], array(T_STRING, T_NS_SEPARATOR))) { + $use .= $tokens[$pos]['content']; + $pos++; + } + + while (in_array($tokens[$pos]['code'], array(T_WHITESPACE, T_AS))) { + $pos++; + } + if ($tokens[$pos]['code'] === T_STRING) { + $alias = $tokens[$pos]['content']; + $pos++; + } + + if ($tokens[$pos]['code'] === T_COMMA) { + $pos++; + if ($tokens[$pos]['code'] === T_WHITESPACE) { + $pos++; + } + } else { // End of uses + $pos = $phpcsFile->findNext(T_USE, $pos); + if ($pos) { + $pos += 2; // Ignore use keywork and whitespace + } + } + + if (!$alias) { + $alias = basename(str_replace('\\', '/', $use)); + } + + $uses[$alias] = $use; + } while ($pos); + return $uses; + } + +/** + * Adjust the throw to use the namespace or aliases names + * + * @param array $throws + * @param string $namespace + * @param array $uses + * @return array + */ + protected function _adjustThrows($throws, $namespace, $uses) { + $formatted = array(); + foreach ($throws as $throw) { + if ($throw[0] === '\\') { // Global + $formatted[] = substr($throw, 1); + continue; + } + + $basename = $throw; + $complement = ''; + if (strpos($basename, '\\') !== false) { + list($basename, $complement) = explode('\\', $basename, 2); + } + + if (isset($uses[$basename])) { + $formatted[] = trim($uses[$basename] . '\\' . $complement, '\\'); + continue; + } + + $formatted[] = trim($namespace . '\\' . $throw, '\\'); + } + + // Only need one @throws tag for each type of exception thrown. + $throws = array_unique($formatted); + sort($throws); + return $throws; + } + +} diff --git a/Sniffs/Formatting/OneClassPerUseSniff.php b/Sniffs/Formatting/OneClassPerUseSniff.php new file mode 100644 index 00000000..ed990083 --- /dev/null +++ b/Sniffs/Formatting/OneClassPerUseSniff.php @@ -0,0 +1,62 @@ + + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version 1.0 + * @link http://pear.php.net/package/PHP_CodeSniffer_CakePHP + */ + +/** + * CakePHP_Sniffs_Formatting_OneClassPerUseSniff. + * + * Ensures the use contains only one class. + * + * @category PHP + * @package PHP_CodeSniffer_CakePHP + * @author Juan Basso + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version 1.0 + * @link http://pear.php.net/package/PHP_CodeSniffer_CakePHP + */ +class CakePHP_Sniffs_Formatting_OneClassPerUseSniff implements PHP_CodeSniffer_Sniff { + +/** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() { + return array(T_USE); + } + +/** + * Processes this test, when one of its tokens is encountered. + * + * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @return void + */ + public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { + $tokens = $phpcsFile->getTokens(); + $i = 2; // Ignore use word and whitespace + $filename = $phpcsFile->getFilename(); + + while (in_array($tokens[$stackPtr + $i]['code'], array(T_STRING, T_NS_SEPARATOR, T_WHITESPACE, T_AS))) { + $i++; + } + + if ($tokens[$stackPtr + $i]['code'] === T_COMMA) { + $error = 'Only one class is allowed per use'; + $phpcsFile->addError($error, $stackPtr, 'OneClassPerUse', array()); + } + } + +} diff --git a/Sniffs/Formatting/UseInAlphabeticalOrderSniff.php b/Sniffs/Formatting/UseInAlphabeticalOrderSniff.php new file mode 100644 index 00000000..b8aec9a6 --- /dev/null +++ b/Sniffs/Formatting/UseInAlphabeticalOrderSniff.php @@ -0,0 +1,102 @@ + + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version 1.0 + * @link http://pear.php.net/package/PHP_CodeSniffer_CakePHP + */ + +/** + * CakePHP_Sniffs_Formatting_UseInAlphabeticalOrderSniff. + * + * Ensures all the use are in alphabetical order. + * + * @category PHP + * @package PHP_CodeSniffer_CakePHP + * @author Juan Basso + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version 1.0 + * @link http://pear.php.net/package/PHP_CodeSniffer_CakePHP + */ +class CakePHP_Sniffs_Formatting_UseInAlphabeticalOrderSniff implements PHP_CodeSniffer_Sniff { + +/** + * Processed files + * + * @var array + */ + protected $_processed = array(); + +/** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() { + return array(T_USE); + } + +/** + * Processes this test, when one of its tokens is encountered. + * + * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @return void + */ + public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { + if (isset($this->_processed[$phpcsFile->getFilename()])) { + return; + } + + $tokens = $phpcsFile->getTokens(); + + $uses = array(); + do { + $scope = 0; + if (!empty($tokens[$stackPtr]['conditions'])) { + $scope = key($tokens[$stackPtr]['conditions']); + } + if (!isset($uses[$scope]['__line__'])) { + $uses[$scope]['__line__'] = $tokens[$stackPtr]['line']; + } + + $stackPtr += 2; // use keyword and whitespace + + $code = ''; + while (!in_array($tokens[$stackPtr]['code'], array(T_SEMICOLON, T_OPEN_CURLY_BRACKET))) { + $code .= $tokens[$stackPtr++]['content']; + } + foreach (explode(',', $code) as $part) { + list($use) = explode(' ', $part); + $use = trim($use, "\n\t\\ "); + } + $uses[$scope][] = $use; + + $stackPtr = $phpcsFile->findNext(T_USE, $stackPtr); + } while ($stackPtr !== false); + + foreach ($uses as $useScope) { + $line = $useScope['__line__']; + unset($useScope['__line__']); + + $ordered = $useScope; + sort($ordered); + + if ($useScope !== $ordered) { + $error = 'Use classes must be in alphabetical order.'; + $phpcsFile->addError($error, $line, 'UseInAlphabeticalOrder', array()); + } + } + + $this->_processed[$phpcsFile->getFilename()] = true; + } + +} diff --git a/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/Sniffs/NamingConventions/ValidFunctionNameSniff.php index 53bcdb7e..894a293b 100644 --- a/Sniffs/NamingConventions/ValidFunctionNameSniff.php +++ b/Sniffs/NamingConventions/ValidFunctionNameSniff.php @@ -14,7 +14,7 @@ */ if (class_exists('PHP_CodeSniffer_Standards_AbstractScopeSniff', true) === false) { - throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_AbstractScopeSniff not found'); + throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_AbstractScopeSniff not found'); } /** diff --git a/Sniffs/NamingConventions/ValidNamespaceNameSniff.php b/Sniffs/NamingConventions/ValidNamespaceNameSniff.php new file mode 100644 index 00000000..e4028ee2 --- /dev/null +++ b/Sniffs/NamingConventions/ValidNamespaceNameSniff.php @@ -0,0 +1,67 @@ + + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version 1.0 + * @link http://pear.php.net/package/PHP_CodeSniffer_CakePHP + */ + +/** + * CakePHP_Sniffs_NamingConventions_ValidNamespaceNameSniff. + * + * Ensures namespace names are correct depending on the folder of the file. + * + * @category PHP + * @package PHP_CodeSniffer_CakePHP + * @author Juan Basso + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version 1.0 + * @link http://pear.php.net/package/PHP_CodeSniffer_CakePHP + */ +class CakePHP_Sniffs_NamingConventions_ValidNamespaceNameSniff implements PHP_CodeSniffer_Sniff { + +/** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() { + return array(T_NAMESPACE); + } + +/** + * Processes this test, when one of its tokens is encountered. + * + * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @return void + */ + public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { + $tokens = $phpcsFile->getTokens(); + $i = 2; // Ignore namespace word and whitespace + $filename = $phpcsFile->getFilename(); + $ns = ''; + + while (!in_array($tokens[$stackPtr + $i]['code'], array(T_SEMICOLON, T_WHITESPACE, T_OPEN_CURLY_BRACKET))) { + $ns .= $tokens[$stackPtr + $i]['content']; + $i++; + } + + $ns = '\\' . ltrim($ns, '\\'); + $path = dirname($filename); + + if (substr(str_replace('/', '\\', $path), -1 * strlen($ns)) !== $ns) { + $error = 'Namespace does not match with the directory name'; + $phpcsFile->addError($error, $stackPtr, 'InvalidNamespace', array()); + } + } + +} diff --git a/Sniffs/NamingConventions/ValidTraitNameSniff.php b/Sniffs/NamingConventions/ValidTraitNameSniff.php new file mode 100644 index 00000000..2f988901 --- /dev/null +++ b/Sniffs/NamingConventions/ValidTraitNameSniff.php @@ -0,0 +1,61 @@ + + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version 1.0 + * @link http://pear.php.net/package/PHP_CodeSniffer_CakePHP + */ + +if (!defined('T_TRAIT')) { + define('T_TRAIT', 355); +} + +/** + * CakePHP_Sniffs_NamingConventions_ValidTraitNameSniff. + * + * Ensures trait names are correct depending on the folder of the file. + * + * @category PHP + * @package PHP_CodeSniffer_CakePHP + * @author Juan Basso + * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version 1.0 + * @link http://pear.php.net/package/PHP_CodeSniffer_CakePHP + */ +class CakePHP_Sniffs_NamingConventions_ValidTraitNameSniff implements PHP_CodeSniffer_Sniff { + +/** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() { + return array(T_TRAIT); + } + +/** + * Processes this test, when one of its tokens is encountered. + * + * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @return void + */ + public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { + $tokens = $phpcsFile->getTokens(); + $traitName = $tokens[$stackPtr + 2]['content']; + + if (substr($traitName, -5) !== 'Trait') { + $error = 'Traits must have a "Trait" suffix.'; + $phpcsFile->addError($error, $stackPtr, 'InvalidTraitName', array()); + } + } + +} diff --git a/Sniffs/WhiteSpace/OperatorSpacingSniff.php b/Sniffs/WhiteSpace/OperatorSpacingSniff.php index 76aa7e6f..2733cbc9 100755 --- a/Sniffs/WhiteSpace/OperatorSpacingSniff.php +++ b/Sniffs/WhiteSpace/OperatorSpacingSniff.php @@ -24,6 +24,7 @@ * @link http://pear.php.net/package/PHP_CodeSniffer */ class CakePHP_Sniffs_WhiteSpace_OperatorSpacingSniff implements PHP_CodeSniffer_Sniff { + /** * A list of tokenizers this sniff supports. * @@ -47,7 +48,6 @@ public function register() { return array_unique(array_merge($comparison, $operators, $assignment)); } - /** * Processes this sniff, when one of its tokens is encountered. * diff --git a/ruleset.xml b/ruleset.xml index 1c18e6de..89432eee 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -19,7 +19,6 @@ - diff --git a/tests/CakePHPStandardTest.php b/tests/CakePHPStandardTest.php index cc4b8174..da9a0af7 100644 --- a/tests/CakePHPStandardTest.php +++ b/tests/CakePHPStandardTest.php @@ -17,14 +17,16 @@ public static function testProvider() { PHPUnit_Framework_TestCase::fail("The dirname for the standard must be CakePHP"); } - $files = scandir(__DIR__ . '/files'); - foreach ($files as $file) { - if ($file[0] === '.') { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__ . '/files')); + foreach ($iterator as $dir) { + if ($dir->isDir()) { continue; } + + $file = $dir->getPathname(); $expectPass = (substr($file, -8) === 'pass.php'); $tests[] = array( - __DIR__ . '/files/' . $file, + $file, $standard, $expectPass ); diff --git a/tests/files/CakePHP/SniffTest/BadNs.php b/tests/files/CakePHP/SniffTest/BadNs.php new file mode 100644 index 00000000..367fb8aa --- /dev/null +++ b/tests/files/CakePHP/SniffTest/BadNs.php @@ -0,0 +1,5 @@ +