diff --git a/package.xml b/package.xml index 381c82dfb0..3f09663042 100644 --- a/package.xml +++ b/package.xml @@ -80,6 +80,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + diff --git a/src/Files/File.php b/src/Files/File.php index eb56ef64b4..a5fdc67e00 100644 --- a/src/Files/File.php +++ b/src/Files/File.php @@ -2355,55 +2355,59 @@ public function getCondition($stackPtr, $type) /** * Returns the name of the class that the specified class extends. - * (works for classes, anonymous classes and interfaces) + * + * Works for classes, anonymous classes and interfaces, though it is + * strongly recommended to use the findExtendedInterfaceNames() method + * to examine interfaces as they can extend multiple parent interfaces. * * Returns FALSE on error or if there is no extended class name. * - * @param int $stackPtr The stack position of the class. + * @param int $stackPtr The stack position of the class keyword. * * @return string|false */ public function findExtendedClassName($stackPtr) { - // Check for the existence of the token. - if (isset($this->tokens[$stackPtr]) === false) { - return false; - } + $validStructures = [ + T_CLASS => true, + T_ANON_CLASS => true, + T_INTERFACE => true, + ]; - if ($this->tokens[$stackPtr]['code'] !== T_CLASS - && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS - && $this->tokens[$stackPtr]['code'] !== T_INTERFACE - ) { - return false; - } + $classes = $this->examineObjectDeclarationSignature($stackPtr, $validStructures, T_EXTENDS); - if (isset($this->tokens[$stackPtr]['scope_opener']) === false) { + if (empty($classes) === true) { return false; } - $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener']; - $extendsIndex = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex); - if (false === $extendsIndex) { - return false; - } + // Classes can only extend one parent class. + return $classes[0]; - $find = [ - T_NS_SEPARATOR, - T_STRING, - T_WHITESPACE, - ]; + }//end findExtendedClassName() - $end = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true); - $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1)); - $name = trim($name); - if ($name === '') { + /** + * Returns the names of the interfaces that the specified interface extends. + * + * Returns FALSE on error or if there is no extended interface name. + * + * @param int $stackPtr The stack position of the interface keyword. + * + * @return array|false + */ + public function findExtendedInterfaceNames($stackPtr) + { + $validStructures = [T_INTERFACE => true]; + + $interfaces = $this->examineObjectDeclarationSignature($stackPtr, $validStructures, T_EXTENDS); + + if (empty($interfaces) === true) { return false; } - return $name; + return $interfaces; - }//end findExtendedClassName() + }//end findExtendedInterfaceNames() /** @@ -2411,53 +2415,93 @@ public function findExtendedClassName($stackPtr) * * Returns FALSE on error or if there are no implemented interface names. * - * @param int $stackPtr The stack position of the class. + * @param int $stackPtr The stack position of the class keyword. * * @return array|false */ public function findImplementedInterfaceNames($stackPtr) + { + $validStructures = [ + T_CLASS => true, + T_ANON_CLASS => true, + ]; + + $interfaces = $this->examineObjectDeclarationSignature($stackPtr, $validStructures, T_IMPLEMENTS); + + if (empty($interfaces) === true) { + return false; + } + + return $interfaces; + + }//end findImplementedInterfaceNames() + + + /** + * Returns the names of the extended classes or interfaces or the implemented + * interfaces that the specific class/interface declaration extends/implements. + * + * Returns FALSE on error or if the object does not extend/implement another + * object. + * + * @param int $stackPtr The stack position of the class/interface keyword. + * @param array $OOTypes Array of accepted token types. + * Array format => true. + * @param int $keyword The keyword to examine. Either `T_EXTENDS` or `T_IMPLEMENTS`. + * + * @return array|false + */ + private function examineObjectDeclarationSignature($stackPtr, $OOTypes, $keyword) { // Check for the existence of the token. if (isset($this->tokens[$stackPtr]) === false) { return false; } - if ($this->tokens[$stackPtr]['code'] !== T_CLASS - && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS - ) { + if (isset($OOTypes[$this->tokens[$stackPtr]['code']]) === false) { return false; } - if (isset($this->tokens[$stackPtr]['scope_closer']) === false) { + if (isset($this->tokens[$stackPtr]['scope_opener']) === false) { return false; } - $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener']; - $implementsIndex = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex); - if ($implementsIndex === false) { + $openerIndex = $this->tokens[$stackPtr]['scope_opener']; + $keywordIndex = $this->findNext($keyword, ($stackPtr + 1), $openerIndex); + if ($keywordIndex === false) { return false; } - $find = [ - T_NS_SEPARATOR, - T_STRING, - T_WHITESPACE, - T_COMMA, - ]; + $find = Util\Tokens::$emptyTokens; + $find[] = T_NS_SEPARATOR; + $find[] = T_STRING; + $find[] = T_COMMA; - $end = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true); - $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1)); - $name = trim($name); + $end = $this->findNext($find, ($keywordIndex + 1), ($openerIndex + 1), true); + $names = []; + $name = ''; + for ($i = ($keywordIndex + 1); $i < $end; $i++) { + if (isset(Util\Tokens::$emptyTokens[$this->tokens[$i]['code']]) === true) { + continue; + } - if ($name === '') { - return false; - } else { - $names = explode(',', $name); - $names = array_map('trim', $names); - return $names; + if ($this->tokens[$i]['code'] === T_COMMA && $name !== '') { + $names[] = $name; + $name = ''; + continue; + } + + $name .= $this->tokens[$i]['content']; } - }//end findImplementedInterfaceNames() + // Add the last name. + if ($name !== '') { + $names[] = $name; + } + + return $names; + + }//end examineObjectDeclarationSignature() }//end class diff --git a/tests/Core/AllTests.php b/tests/Core/AllTests.php index 36a98a9ba7..0fd0235467 100644 --- a/tests/Core/AllTests.php +++ b/tests/Core/AllTests.php @@ -16,6 +16,7 @@ require_once 'ErrorSuppressionTest.php'; require_once 'File/FindEndOfStatementTest.php'; require_once 'File/FindExtendedClassNameTest.php'; +require_once 'File/FindExtendedInterfaceNamesTest.php'; require_once 'File/FindImplementedInterfaceNamesTest.php'; require_once 'File/GetMemberPropertiesTest.php'; require_once 'File/GetMethodParametersTest.php'; @@ -50,6 +51,7 @@ public static function suite() $suite->addTestSuite('PHP_CodeSniffer\Tests\Core\ErrorSuppressionTest'); $suite->addTestSuite('PHP_CodeSniffer\Tests\Core\File\FindEndOfStatementTest'); $suite->addTestSuite('PHP_CodeSniffer\Tests\Core\File\FindExtendedClassNameTest'); + $suite->addTestSuite('PHP_CodeSniffer\Tests\Core\File\FindExtendedInterfaceNamesTest'); $suite->addTestSuite('PHP_CodeSniffer\Tests\Core\File\FindImplementedInterfaceNamesTest'); $suite->addTestSuite('PHP_CodeSniffer\Tests\Core\File\GetMemberPropertiesTest'); $suite->addTestSuite('PHP_CodeSniffer\Tests\Core\File\GetMethodParametersTest'); diff --git a/tests/Core/File/FindExtendedInterfaceNamesTest.inc b/tests/Core/File/FindExtendedInterfaceNamesTest.inc new file mode 100644 index 0000000000..fa37dada49 --- /dev/null +++ b/tests/Core/File/FindExtendedInterfaceNamesTest.inc @@ -0,0 +1,31 @@ + + * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\File; + +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Ruleset; +use PHP_CodeSniffer\Files\DummyFile; +use PHPUnit\Framework\TestCase; + +class FindExtendedInterfaceNamesTest extends TestCase +{ + + /** + * The \PHP_CodeSniffer\Files\File object containing parsed contents of the test case file. + * + * @var \PHP_CodeSniffer\Files\File + */ + private $phpcsFile; + + + /** + * Initialize & tokenize \PHP_CodeSniffer\Files\File with code from the test case file. + * + * Methods used for these tests can be found in a test case file in the same + * directory and with the same name, using the .inc extension. + * + * @return void + */ + public function setUp() + { + $config = new Config(); + $config->standards = ['Generic']; + + $ruleset = new Ruleset($config); + + $pathToTestFile = dirname(__FILE__).'/'.basename(__FILE__, '.php').'.inc'; + $this->phpcsFile = new DummyFile(file_get_contents($pathToTestFile), $ruleset, $config); + $this->phpcsFile->process(); + + }//end setUp() + + + /** + * Clean up after finished test. + * + * @return void + */ + public function tearDown() + { + unset($this->phpcsFile); + + }//end tearDown() + + + /** + * Test retrieving the names of the interfaces being extended by another interface. + * + * @param string $identifier Comment which preceeds the test case. + * @param bool $expected Expected function output. + * + * @dataProvider dataExtendedInterface + * + * @return void + */ + public function testFindExtendedInterfaceNames($identifier, $expected) + { + $start = ($this->phpcsFile->numTokens - 1); + $delim = $this->phpcsFile->findPrevious( + T_COMMENT, + $start, + null, + false, + $identifier + ); + $interface = $this->phpcsFile->findNext(T_INTERFACE, ($delim + 1)); + + $result = $this->phpcsFile->findExtendedInterfaceNames($interface); + $this->assertSame($expected, $result); + + }//end testFindExtendedInterfaceNames() + + + /** + * Data provider for the FindExtendedInterfaceNames test. + * + * @see testFindExtendedInterfaceNames() + * + * @return array + */ + public function dataExtendedInterface() + { + return [ + [ + '/* testInterface */', + false, + ], + [ + '/* testExtendedInterface */', + ['testFEINInterface'], + ], + [ + '/* testMultiExtendedInterface */', + [ + 'testFEINInterface', + 'testFEINInterface2', + ], + ], + [ + '/* testNamespacedInterface */', + ['\PHP_CodeSniffer\Tests\Core\File\testFEINInterface'], + ], + [ + '/* testMultiNamespacedInterface */', + [ + '\PHP_CodeSniffer\Tests\Core\File\testFEINInterface', + '\PHP_CodeSniffer\Tests\Core\File\testFEINInterface2', + ], + ], + [ + '/* testMultiExtendedInterfaceWithComment */', + [ + 'testFEINInterface', + '\PHP_CodeSniffer\Tests\Core\File\testFEINInterface2', + '\testFEINInterface3', + ], + ], + ]; + + }//end dataExtendedInterface() + + +}//end class