Skip to content

Commit

Permalink
Merge pull request #144 from jrfnl/feature/new-interfaces-sniff
Browse files Browse the repository at this point in the history
Two new sniffs for Interfaces
  • Loading branch information
wimg committed Aug 4, 2016
2 parents a0c8a1f + 3aafcc4 commit aee42ae
Show file tree
Hide file tree
Showing 7 changed files with 651 additions and 0 deletions.
60 changes: 60 additions & 0 deletions Sniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,64 @@ public function supportsBelow($phpVersion)
}
}//end supportsBelow()

/**
* Returns the name(s) of the interface(s) that the specified class implements.
*
* Returns FALSE on error or if there are no implemented interface names.
*
* {@internal Duplicate of same method as introduced in PHPCS 2.7.
* Once the minimum supported PHPCS version for this sniff library goes beyond
* that, this method can be removed and call to it replaced with
* `$phpcsFile->findImplementedInterfaceNames($stackPtr)` calls.}}
*
* @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the class token.
*
* @return array|false
*/
public function findImplementedInterfaceNames($phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();

// Check for the existence of the token.
if (isset($tokens[$stackPtr]) === false) {
return false;
}

if ($tokens[$stackPtr]['code'] !== T_CLASS) {
return false;
}

if (isset($tokens[$stackPtr]['scope_closer']) === false) {
return false;
}

$classOpenerIndex = $tokens[$stackPtr]['scope_opener'];
$implementsIndex = $phpcsFile->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
if ($implementsIndex === false) {
return false;
}

$find = array(
T_NS_SEPARATOR,
T_STRING,
T_WHITESPACE,
T_COMMA,
);

$end = $phpcsFile->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
$name = $phpcsFile->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
$name = trim($name);

if ($name === '') {
return false;
} else {
$names = explode(',', $name);
$names = array_map('trim', $names);
return $names;
}

}//end findImplementedInterfaceNames()


}//end class
84 changes: 84 additions & 0 deletions Sniffs/PHP/InternalInterfacesSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php
/**
* PHPCompatibility_Sniffs_PHP_InternalInterfacesSniff.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/

/**
* PHPCompatibility_Sniffs_PHP_InternalInterfacesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class PHPCompatibility_Sniffs_PHP_InternalInterfacesSniff extends PHPCompatibility_Sniff
{

/**
* A list of PHP internal interfaces, not intended to be implemented by userland classes.
*
* The array lists : the error message to use.
*
* @var array(string => string)
*/
protected $internalInterfaces = array(
'Traversable' => 'shouldn\'t be implemented directly, implement the Iterator or IteratorAggregate interface instead.',
'DateTimeInterface' => 'is intended for type hints only and is not implementable.',
'Throwable' => 'cannot be implemented directly, extend the Exception class instead.',
);


/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Handle case-insensitivity of class names.
$keys = array_keys( $this->internalInterfaces );
$keys = array_map( 'strtolower', $keys );
$this->internalInterfaces = array_combine( $keys, $this->internalInterfaces );

return array(T_CLASS);

}//end register()


/**
* 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)
{
$interfaces = $this->findImplementedInterfaceNames($phpcsFile, $stackPtr);

if (is_array($interfaces) === false || $interfaces === array()) {
return;
}

foreach ($interfaces as $interface) {
$lcInterface = strtolower($interface);
if (isset($this->internalInterfaces[$lcInterface]) === true) {
$error = 'The interface %s %s';
$data = array(
$interface,
$this->internalInterfaces[$lcInterface],
);
$phpcsFile->addError($error, $stackPtr, 'Found', $data);
}
}

}//end process()

}//end class
198 changes: 198 additions & 0 deletions Sniffs/PHP/NewInterfacesSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php
/**
* PHPCompatibility_Sniffs_PHP_NewInterfacesSniff.
*
* PHP version 5.5
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/

/**
* PHPCompatibility_Sniffs_PHP_NewInterfacesSniff.
*
* @category PHP
* @package PHPCompatibility
* @author Juliette Reinders Folmer <phpcompatibility_nospam@adviesenzo.nl>
*/
class PHPCompatibility_Sniffs_PHP_NewInterfacesSniff extends PHPCompatibility_Sniff
{

/**
* A list of new interfaces, not present in older versions.
*
* The array lists : version number with false (not present) or true (present).
* If's sufficient to list the first version where the interface appears.
*
* @var array(string => array(string => int|string|null))
*/
protected $newInterfaces = array(
'Countable' => array(
'5.0' => false,
'5.1' => true
),
'OuterIterator' => array(
'5.0' => false,
'5.1' => true
),
'RecursiveIterator' => array(
'5.0' => false,
'5.1' => true
),
'SeekableIterator' => array(
'5.0' => false,
'5.1' => true
),
'Serializable' => array(
'5.0' => false,
'5.1' => true,
),
'SplObserver' => array(
'5.0' => false,
'5.1' => true
),
'SplSubject' => array(
'5.0' => false,
'5.1' => true
),

'JsonSerializable' => array(
'5.3' => false,
'5.4' => true
),
'SessionHandlerInterface' => array(
'5.3' => false,
'5.4' => true
),

);

/**
* A list of methods which cannot be used in combination with particular interfaces.
*
* @var array(string => array(string => string))
*/
protected $unsupportedMethods = array(
'Serializable' => array(
'__sleep' => 'http://php.net/serializable',
'__wakeup' => 'http://php.net/serializable',
),
);

/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
// Handle case-insensitivity of class names.
$keys = array_keys( $this->newInterfaces );
$keys = array_map( 'strtolower', $keys );
$this->newInterfaces = array_combine( $keys, $this->newInterfaces );

$keys = array_keys( $this->unsupportedMethods );
$keys = array_map( 'strtolower', $keys );
$this->unsupportedMethods = array_combine( $keys, $this->unsupportedMethods );

return array(T_CLASS);

}//end register()


/**
* 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)
{
$interfaces = $this->findImplementedInterfaceNames($phpcsFile, $stackPtr);

if (is_array($interfaces) === false || $interfaces === array()) {
return;
}

$tokens = $phpcsFile->getTokens();
$checkMethods = false;

if(isset($tokens[$stackPtr]['scope_closer'])) {
$checkMethods = true;
$scopeCloser = $tokens[$stackPtr]['scope_closer'];
}

foreach ($interfaces as $interface) {
$lcInterface = strtolower($interface);
if (isset($this->newInterfaces[$lcInterface]) === true) {
$this->addError($phpcsFile, $stackPtr, $interface);
}

if ($checkMethods === true && isset($this->unsupportedMethods[$lcInterface]) === true) {
$nextFunc = $stackPtr;
while (($nextFunc = $phpcsFile->findNext(T_FUNCTION, ($nextFunc + 1), $scopeCloser)) !== false) {
$funcNamePos = $phpcsFile->findNext(T_STRING, $nextFunc);
$funcName = strtolower($tokens[$funcNamePos]['content']);

if (isset($this->unsupportedMethods[$lcInterface][$funcName]) === true) {
$error = 'Classes that implement interface %s do not support the method %s(). See %s';
$data = array(
$interface,
$tokens[$funcNamePos]['content'],
$this->unsupportedMethods[$lcInterface][$funcName],
);
$phpcsFile->addError($error, $funcNamePos, 'UnsupportedMethod', $data);
}
}
}
}

}//end process()


/**
* Generates the error or warning for this sniff.
*
* @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the function
* in the token array.
* @param string $interface The name of the interface.
* @param string $pattern The pattern used for the match.
*
* @return void
*/
protected function addError($phpcsFile, $stackPtr, $interface, $pattern=null)
{
if ($pattern === null) {
$pattern = strtolower($interface);
}

$error = '';

$isError = false;
foreach ($this->newInterfaces[$pattern] as $version => $present) {
if ($this->supportsBelow($version)) {
if ($present === false) {
$isError = true;
$error .= 'not present in PHP version ' . $version . ' or earlier';
}
}
}

if (strlen($error) > 0) {
$error = 'The built-in interface ' . $interface . ' is ' . $error;

if ($isError === true) {
$phpcsFile->addError($error, $stackPtr);
} else {
$phpcsFile->addWarning($error, $stackPtr);
}
}

}//end addError()

}//end class

0 comments on commit aee42ae

Please sign in to comment.