Skip to content

Commit

Permalink
Merge pull request #11 from cakephp/new-rules
Browse files Browse the repository at this point in the history
New rules
  • Loading branch information
jrbasso committed Oct 18, 2012
2 parents 6d4cb2f + 037f172 commit a3f25ed
Show file tree
Hide file tree
Showing 20 changed files with 673 additions and 7 deletions.
271 changes: 271 additions & 0 deletions Sniffs/Commenting/FunctionCommentThrowTagSniff.php
@@ -0,0 +1,271 @@
<?php
/**
* CakePHP_Sniffs_Commenting_FunctionCommentThrowTagSniff
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer_CakePHP
* @author Juan Basso <jrbasso@gmail.com>
* @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 <jrbasso@gmail.com>
* @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;
}

}
62 changes: 62 additions & 0 deletions Sniffs/Formatting/OneClassPerUseSniff.php
@@ -0,0 +1,62 @@
<?php
/**
* CakePHP_Sniffs_Formatting_OneClassPerUseSniff
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer_CakePHP
* @author Juan Basso <jrbasso@gmail.com>
* @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 <jrbasso@gmail.com>
* @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());
}
}

}

0 comments on commit a3f25ed

Please sign in to comment.