From 3e6a91a07fd272523043e79580696f54f0009c8f Mon Sep 17 00:00:00 2001 From: Simon Welsh Date: Sat, 17 Dec 2011 14:03:53 +1300 Subject: [PATCH] ENHANCEMENT Allow ClassManifest to handle classes with namespaces, or that extend classes in namspaces or that implement interfaces in namespaces. --- core/manifest/ClassManifest.php | 119 ++++++++++++++---- .../manifest/NamespacedClassManifestTest.php | 114 +++++++++++++++++ .../TokenisedRegularExpressionTest.php | 111 ++++++++++++++-- .../module/_config.php | 0 .../module/classes/ClassA.php | 8 ++ .../module/classes/ClassB.php | 8 ++ .../module/classes/ClassC.php | 8 ++ .../module/classes/ClassD.php | 8 ++ .../module/classes/ClassE.php | 8 ++ .../module/classes/ClassF.php | 8 ++ .../module/classes/ClassG.php | 8 ++ .../module/interfaces/InterfaceA.php | 8 ++ 12 files changed, 372 insertions(+), 36 deletions(-) create mode 100644 tests/core/manifest/NamespacedClassManifestTest.php create mode 100644 tests/core/manifest/fixtures/namespaced_classmanifest/module/_config.php create mode 100644 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassA.php create mode 100644 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassB.php create mode 100644 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassC.php create mode 100644 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassD.php create mode 100644 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassE.php create mode 100644 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassF.php create mode 100644 tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassG.php create mode 100644 tests/core/manifest/fixtures/namespaced_classmanifest/module/interfaces/InterfaceA.php diff --git a/core/manifest/ClassManifest.php b/core/manifest/ClassManifest.php index f4f512038..e10507765 100644 --- a/core/manifest/ClassManifest.php +++ b/core/manifest/ClassManifest.php @@ -40,19 +40,59 @@ class SS_ClassManifest { 3 => T_WHITESPACE, 4 => T_EXTENDS, 5 => T_WHITESPACE, - 6 => array(T_STRING, 'save_to' => 'extends', 'can_jump_to' => 14), + 6 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => 14), 7 => T_WHITESPACE, 8 => T_IMPLEMENTS, 9 => T_WHITESPACE, 10 => array(T_STRING, 'can_jump_to' => 14, 'save_to' => 'interfaces[]'), 11 => array(T_WHITESPACE, 'optional' => true), - 12 => array(',', 'can_jump_to' => 10), + 12 => array(',', 'can_jump_to' => 10, 'save_to' => 'interfaces[]'), 13 => array(T_WHITESPACE, 'can_jump_to' => 10), 14 => array(T_WHITESPACE, 'optional' => true), 15 => '{', )); } - + + /** + * @return TokenisedRegularExpression + */ + public static function get_namespaced_class_parser() { + return new TokenisedRegularExpression(array( + 0 => T_CLASS, + 1 => T_WHITESPACE, + 2 => array(T_STRING, 'can_jump_to' => array(8, 16), 'save_to' => 'className'), + 3 => T_WHITESPACE, + 4 => T_EXTENDS, + 5 => T_WHITESPACE, + 6 => array(T_NS_SEPARATOR, 'save_to' => 'extends[]', 'optional' => true), + 7 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => array(6, 16)), + 8 => T_WHITESPACE, + 9 => T_IMPLEMENTS, + 10 => T_WHITESPACE, + 11 => array(T_NS_SEPARATOR, 'save_to' => 'interfaces[]', 'optional' => true), + 12 => array(T_STRING, 'can_jump_to' => array(11, 16), 'save_to' => 'interfaces[]'), + 13 => array(T_WHITESPACE, 'optional' => true), + 14 => array(',', 'can_jump_to' => 11, 'save_to' => 'interfaces[]'), + 15 => array(T_WHITESPACE, 'can_jump_to' => 11), + 16 => array(T_WHITESPACE, 'optional' => true), + 17 => '{', + )); + } + + /** + * @return TokenisedRegularExpression + */ + public static function get_namespace_parser() { + return new TokenisedRegularExpression(array( + 0 => T_NAMESPACE, + 1 => T_WHITESPACE, + 2 => array(T_NS_SEPARATOR, 'save_to' => 'namespaceName[]', 'optional' => true), + 3 => array(T_STRING, 'save_to' => 'namespaceName[]', 'can_jump_to' => 2), + 4 => array(T_WHITESPACE, 'optional' => true), + 5 => ';', + )); + } + /** * @return TokenisedRegularExpression */ @@ -63,7 +103,7 @@ class SS_ClassManifest { 2 => array(T_STRING, 'save_to' => 'interfaceName') )); } - + /** * Constructs and initialises a new class manifest, either loading the data * from the cache or re-scanning for classes. @@ -228,7 +268,7 @@ class SS_ClassManifest { 'classes', 'roots', 'children', 'descendants', 'interfaces', 'implementors', 'configs' ); - + // Reset the manifest so stale info doesn't cause errors. foreach ($reset as $reset) { $this->$reset = array(); @@ -267,6 +307,7 @@ class SS_ClassManifest { $classes = null; $interfaces = null; + $namespace = null; // The results of individual file parses are cached, since only a few // files will have changed and TokenisedRegularExpression is quite @@ -277,29 +318,47 @@ class SS_ClassManifest { if ($data = $this->cache->load($key)) { $valid = ( - isset($data['classes']) && isset($data['interfaces']) - && is_array($data['classes']) && is_array($data['interfaces']) + isset($data['classes']) && isset($data['interfaces']) && isset($data['namespace']) + && is_array($data['classes']) && is_array($data['interfaces']) && is_array($data['namespace']) ); if ($valid) { $classes = $data['classes']; $interfaces = $data['interfaces']; + $namespace = $data['namespace']; } } - + if (!$classes) { $tokens = token_get_all($file); - $classes = self::get_class_parser()->findAll($tokens); + if(version_compare(PHP_VERSION, '5.3', '>=')) { + $classes = self::get_namespaced_class_parser()->findAll($tokens); + $namespace = self::get_namespace_parser()->findAll($tokens); + if($namespace) { + $namespace = implode('', $namespace[0]['namespaceName']) . '\\'; + } else { + $namespace = ''; + } + } else { + $classes = self::get_class_parser()->findAll($tokens); + $namespace = ''; + } $interfaces = self::get_interface_parser()->findAll($tokens); - $cache = array('classes' => $classes, 'interfaces' => $interfaces); + $cache = array('classes' => $classes, 'interfaces' => $interfaces, 'namespace' => $namespace); $this->cache->save($cache, $key, array('fileparse')); } foreach ($classes as $class) { - $name = $class['className']; - $extends = isset($class['extends']) ? $class['extends'] : null; + $name = $namespace . $class['className']; + $extends = isset($class['extends']) ? implode('', $class['extends']) : null; $implements = isset($class['interfaces']) ? $class['interfaces'] : null; + + if($extends && $extends[0] != '/\\') { + $extends = $namespace . $extends; + } elseif($extends) { + $extends = substr($extends, 1); + } if (array_key_exists($name, $this->classes)) { throw new Exception(sprintf( @@ -321,20 +380,34 @@ class SS_ClassManifest { } else { $this->roots[] = $name; } - - if ($implements) foreach ($implements as $interface) { - $interface = strtolower($interface); - - if (!isset($this->implementors[$interface])) { - $this->implementors[$interface] = array($name); - } else { - $this->implementors[$interface][] = $name; + + if ($implements) { + $interface = $namespace; + for($i = 0; $i < count($implements); ++$i) { + if($implements[$i] == ',') { + $interface = $namespace; + continue; + } + if($implements[$i] == '\\' && $interface == $namespace) { + $interface = ''; + } else { + $interface .= $implements[$i]; + } + if($i == count($implements)-1 || $implements[$i+1] == ',') { + $interface = strtolower($interface); + + if (!isset($this->implementors[$interface])) { + $this->implementors[$interface] = array($name); + } else { + $this->implementors[$interface][] = $name; + } + } } } } - + foreach ($interfaces as $interface) { - $this->interfaces[strtolower($interface['interfaceName'])] = $pathname; + $this->interfaces[strtolower($namespace . $interface['interfaceName'])] = $pathname; } } @@ -348,7 +421,7 @@ class SS_ClassManifest { protected function coalesceDescendants($class) { $result = array(); $lClass = strtolower($class); - + if (array_key_exists($lClass, $this->children)) { $this->descendants[$lClass] = array(); diff --git a/tests/core/manifest/NamespacedClassManifestTest.php b/tests/core/manifest/NamespacedClassManifestTest.php new file mode 100644 index 000000000..94f8c1978 --- /dev/null +++ b/tests/core/manifest/NamespacedClassManifestTest.php @@ -0,0 +1,114 @@ +base = dirname(__FILE__) . '/fixtures/namespaced_classmanifest'; + $this->manifest = new SS_ClassManifest($this->base, false, true, false); + } + + public function testGetItemPath() { + $expect = array( + 'SAPPHIRE\TEST\CLASSA' => 'module/classes/ClassA.php', + 'Sapphire\Test\ClassA' => 'module/classes/ClassA.php', + 'sapphire\test\classa' => 'module/classes/ClassA.php', + 'SAPPHIRE\TEST\INTERFACEA' => 'module/interfaces/InterfaceA.php', + 'Sapphire\Test\InterfaceA' => 'module/interfaces/InterfaceA.php', + 'sapphire\test\interfacea' => 'module/interfaces/InterfaceA.php' + ); + + foreach ($expect as $name => $path) { + $this->assertEquals("{$this->base}/$path", $this->manifest->getItemPath($name)); + } + } + + public function testGetClasses() { + $expect = array( + 'sapphire\test\classa' => "{$this->base}/module/classes/ClassA.php", + 'sapphire\test\classb' => "{$this->base}/module/classes/ClassB.php", + 'sapphire\test\classc' => "{$this->base}/module/classes/ClassC.php", + 'sapphire\test\classd' => "{$this->base}/module/classes/ClassD.php", + 'sapphire\test\classe' => "{$this->base}/module/classes/ClassE.php", + 'sapphire\test\classf' => "{$this->base}/module/classes/ClassF.php", + 'sapphire\test\classg' => "{$this->base}/module/classes/ClassG.php" + ); + + $this->assertEquals($expect, $this->manifest->getClasses()); + } + + public function testGetClassNames() { + $this->assertEquals( + array('sapphire\test\classa', 'sapphire\test\classb', 'sapphire\test\classc', 'sapphire\test\classd', 'sapphire\test\classe', 'sapphire\test\classf', 'sapphire\test\classg'), + $this->manifest->getClassNames()); + } + + public function testGetDescendants() { + $expect = array( + 'sapphire\test\classa' => array('sapphire\test\ClassB') + ); + + $this->assertEquals($expect, $this->manifest->getDescendants()); + } + + public function testGetDescendantsOf() { + $expect = array( + 'SAPPHIRE\TEST\CLASSA' => array('sapphire\test\ClassB'), + 'sapphire\test\classa' => array('sapphire\test\ClassB'), + ); + + foreach ($expect as $class => $desc) { + $this->assertEquals($desc, $this->manifest->getDescendantsOf($class)); + } + } + + public function testGetInterfaces() { + $expect = array( + 'sapphire\test\interfacea' => "{$this->base}/module/interfaces/InterfaceA.php", + ); + $this->assertEquals($expect, $this->manifest->getInterfaces()); + } + + public function testGetImplementors() { + $expect = array( + 'sapphire\test\interfacea' => array('sapphire\test\ClassE'), + 'interfacea' => array('sapphire\test\ClassF'), + 'sapphire\test\subtest\interfacea' => array('sapphire\test\ClassG') + ); + $this->assertEquals($expect, $this->manifest->getImplementors()); + } + + public function testGetImplementorsOf() { + $expect = array( + 'SAPPHIRE\TEST\INTERFACEA' => array('sapphire\test\ClassE'), + 'sapphire\test\interfacea' => array('sapphire\test\ClassE'), + 'INTERFACEA' => array('sapphire\test\ClassF'), + 'interfacea' => array('sapphire\test\ClassF'), + 'SAPPHIRE\TEST\SUBTEST\INTERFACEA' => array('sapphire\test\ClassG'), + 'sapphire\test\subtest\interfacea' => array('sapphire\test\ClassG'), + ); + + foreach ($expect as $interface => $impl) { + $this->assertEquals($impl, $this->manifest->getImplementorsOf($interface)); + } + } + + public function testGetConfigs() { + $expect = array("{$this->base}/module/_config.php"); + $this->assertEquals($expect, $this->manifest->getConfigs()); + } + + public function testGetModules() { + $expect = array("module" => "{$this->base}/module"); + $this->assertEquals($expect, $this->manifest->getModules()); + } +} \ No newline at end of file diff --git a/tests/core/manifest/TokenisedRegularExpressionTest.php b/tests/core/manifest/TokenisedRegularExpressionTest.php index 509c1e211..843301e14 100644 --- a/tests/core/manifest/TokenisedRegularExpressionTest.php +++ b/tests/core/manifest/TokenisedRegularExpressionTest.php @@ -47,6 +47,45 @@ interface InterfaceC extends InterfaceA, InterfaceB { } interface InterfaceD extends InterfaceA, InterfaceB, InterfaceC { } +?> +PHP +); + } + + function getNamespaceTokens() { + return token_get_all(<< PHP ); @@ -56,44 +95,90 @@ PHP $parser = SS_ClassManifest::get_class_parser(); $tokens = $this->getTokens(); - + $matches = $parser->findAll($tokens); $classes = array(); if($matches) foreach($matches as $match) $classes[$match['className']] = $match; - + $this->assertArrayHasKey('ClassA', $classes); $this->assertArrayHasKey('ClassB', $classes); - + $this->assertArrayHasKey('ClassC', $classes); - $this->assertEquals('ParentClassC', $classes['ClassC']['extends']); - + $this->assertEquals(array('ParentClassC'), $classes['ClassC']['extends']); + $this->assertArrayHasKey('ClassD', $classes); - $this->assertEquals('ParentClassD', $classes['ClassD']['extends']); + $this->assertEquals(array('ParentClassD'), $classes['ClassD']['extends']); $this->assertContains('InterfaceA', $classes['ClassD']['interfaces']); - + $this->assertArrayHasKey('ClassE', $classes); - $this->assertEquals('ParentClassE', $classes['ClassE']['extends']); + $this->assertEquals(array('ParentClassE'), $classes['ClassE']['extends']); $this->assertContains('InterfaceA', $classes['ClassE']['interfaces']); $this->assertContains('InterfaceB', $classes['ClassE']['interfaces']); - + $this->assertArrayHasKey('ClassF', $classes); - $this->assertEquals('ParentClassF', $classes['ClassF']['extends']); + $this->assertEquals(array('ParentClassF'), $classes['ClassF']['extends']); $this->assertContains('InterfaceA', $classes['ClassF']['interfaces']); $this->assertContains('InterfaceB', $classes['ClassF']['interfaces']); } - + + function testNamesapcedClassDefParser() { + if(version_compare(PHP_VERSION, '5.3', '<')) { + return; + } + $parser = SS_ClassManifest::get_namespaced_class_parser(); + + $tokens = $this->getNamespaceTokens(); + + $matches = $parser->findAll($tokens); + + $classes = array(); + if($matches) foreach($matches as $match) $classes[$match['className']] = $match; + + $this->assertArrayHasKey('ClassA', $classes); + $this->assertArrayHasKey('ClassB', $classes); + $this->assertEquals(array('ParentClassB'), $classes['ClassB']['extends']); + + $this->assertArrayHasKey('ClassC', $classes); + $this->assertEquals(array('\\', 'ParentClassC'), $classes['ClassC']['extends']); + + $this->assertArrayHasKey('ClassD', $classes); + $this->assertEquals(array('subtest', '\\', 'ParentClassD'), $classes['ClassD']['extends']); + + $this->assertArrayHasKey('ClassE', $classes); + $this->assertContains('InterfaceE', $classes['ClassE']['interfaces']); + + $this->assertArrayHasKey('ClassF', $classes); + $this->assertEquals(array('\\', 'InterfaceF'), $classes['ClassF']['interfaces']); + } + function testInterfaceDefParser() { $parser = SS_ClassManifest::get_interface_parser(); $tokens = $this->getTokens(); - + $matches = $parser->findAll($tokens); $interfaces = array(); if($matches) foreach($matches as $match) $interfaces[$match['interfaceName']] = $match; - + $this->assertArrayHasKey('InterfaceA', $interfaces); $this->assertArrayHasKey('InterfaceB', $interfaces); $this->assertArrayHasKey('InterfaceC', $interfaces); $this->assertArrayHasKey('InterfaceD', $interfaces); } + + function testNamespaceDefParser() { + if(version_compare(PHP_VERSION, '5.3', '<')) { + return; + } + $parser = SS_ClassManifest::get_namespace_parser(); + + $namespacedTokens = $this->getNamespaceTokens(); + $tokens = $this->getTokens(); + + $namespacedMatches = $parser->findAll($namespacedTokens); + $matches = $parser->findAll($tokens); + + $this->assertEquals(array(), $matches); + $this->assertEquals(array('sapphire', '\\', 'test'), $namespacedMatches[0]['namespaceName']); + } } \ No newline at end of file diff --git a/tests/core/manifest/fixtures/namespaced_classmanifest/module/_config.php b/tests/core/manifest/fixtures/namespaced_classmanifest/module/_config.php new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassA.php b/tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassA.php new file mode 100644 index 000000000..47ecc7cf6 --- /dev/null +++ b/tests/core/manifest/fixtures/namespaced_classmanifest/module/classes/ClassA.php @@ -0,0 +1,8 @@ +