ENHANCEMENT Allow ClassManifest to handle classes with namespaces, or that extend classes in namspaces or that implement interfaces in namespaces.

This commit is contained in:
Simon Welsh 2011-12-17 14:03:53 +13:00
parent bad1b88942
commit 3e6a91a07f
12 changed files with 372 additions and 36 deletions

View File

@ -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();

View File

@ -0,0 +1,114 @@
<?php
/**
* Tests for the {@link SS_ClassManifest} class.
*
* @package sapphire
* @subpackage tests
*/
class NamespacedClassManifestTest extends SapphireTest {
protected $base;
protected $manifest;
public function setUp() {
parent::setUp();
$this->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());
}
}

View File

@ -47,6 +47,45 @@ interface InterfaceC extends InterfaceA, InterfaceB {
}
interface InterfaceD extends InterfaceA, InterfaceB, InterfaceC {
}
?>
PHP
);
}
function getNamespaceTokens() {
return token_get_all(<<<PHP
<?php
namespace sapphire\\test;
class ClassA {
}
class ClassB extends ParentClassB {
}
class ClassC extends \\ParentClassC {
}
class ClassD extends subtest\\ParentClassD {
}
class ClassE implements InterfaceE {
}
class ClassF implements \\InterfaceF {
}
class ClassG implements subtest\\InterfaceG {
}
?>
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']);
}
}

View File

@ -0,0 +1,8 @@
<?php
/**
* @ignore
*/
namespace sapphire\test;
class ClassA { }

View File

@ -0,0 +1,8 @@
<?php
/**
* @ignore
*/
namespace sapphire\test;
class ClassB extends ClassA { }

View File

@ -0,0 +1,8 @@
<?php
/**
* @ignore
*/
namespace sapphire\test;
class ClassC extends \ClassA { }

View File

@ -0,0 +1,8 @@
<?php
/**
* @ignore
*/
namespace sapphire\test;
class ClassD extends subtest\ClassC { }

View File

@ -0,0 +1,8 @@
<?php
/**
* @ignore
*/
namespace sapphire\test;
class ClassE implements InterfaceA { }

View File

@ -0,0 +1,8 @@
<?php
/**
* @ignore
*/
namespace sapphire\test;
class ClassF implements \InterfaceA { }

View File

@ -0,0 +1,8 @@
<?php
/**
* @ignore
*/
namespace sapphire\test;
class ClassG implements subtest\InterfaceA { }

View File

@ -0,0 +1,8 @@
<?php
/**
* @ignore
*/
namespace sapphire\test;
interface InterfaceA { }